commit 247e14703d0d8d6f30548640d4eba87e31a416b2 Author: shenzuqiang Date: Thu May 7 09:56:31 2026 +0800 init: 1、完整的项目初始化。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2ff201 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/AppScope/app.json5 b/AppScope/app.json5 new file mode 100644 index 0000000..3278a64 --- /dev/null +++ b/AppScope/app.json5 @@ -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" + } +} diff --git a/AppScope/resources/base/element/string.json b/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..c9077ec --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "截图兔" + } + ] +} diff --git a/AppScope/resources/base/media/background.png b/AppScope/resources/base/media/background.png new file mode 100644 index 0000000..923f2b3 Binary files /dev/null and b/AppScope/resources/base/media/background.png differ diff --git a/AppScope/resources/base/media/foreground.png b/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000..8f8a39a Binary files /dev/null and b/AppScope/resources/base/media/foreground.png differ diff --git a/AppScope/resources/base/media/layered_image.json b/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000..fb49920 --- /dev/null +++ b/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/PROXYAI.md b/PROXYAI.md new file mode 100644 index 0000000..88f3aa6 --- /dev/null +++ b/PROXYAI.md @@ -0,0 +1,3 @@ +# ProxyAI Instructions + +Describe goals, conventions, risky areas, and review rules here. \ No newline at end of file diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000..5de5435 --- /dev/null +++ b/build-profile.json5 @@ -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" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/code-linter.json5 b/code-linter.json5 new file mode 100644 index 0000000..073990f --- /dev/null +++ b/code-linter.json5 @@ -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" + } +} \ No newline at end of file diff --git a/common/BuildProfile.ets b/common/BuildProfile.ets new file mode 100644 index 0000000..a35b214 --- /dev/null +++ b/common/BuildProfile.ets @@ -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; +} \ No newline at end of file diff --git a/common/Index.ets b/common/Index.ets new file mode 100644 index 0000000..cfb6552 --- /dev/null +++ b/common/Index.ets @@ -0,0 +1,3 @@ +export { Logger } from './src/main/ets/utils/Logger'; + +export { BreakpointSystem, BreakPointType } from './src/main/ets/utils/BreakpointSystem'; \ No newline at end of file diff --git a/common/build-profile.json5 b/common/build-profile.json5 new file mode 100644 index 0000000..4f2a475 --- /dev/null +++ b/common/build-profile.json5 @@ -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" + } + ] +} diff --git a/common/consumer-rules.txt b/common/consumer-rules.txt new file mode 100644 index 0000000..e69de29 diff --git a/common/hvigorfile.ts b/common/hvigorfile.ts new file mode 100644 index 0000000..805c5d7 --- /dev/null +++ b/common/hvigorfile.ts @@ -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. */ +} \ No newline at end of file diff --git a/common/obfuscation-rules.txt b/common/obfuscation-rules.txt new file mode 100644 index 0000000..1e7e54e --- /dev/null +++ b/common/obfuscation-rules.txt @@ -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 \ No newline at end of file diff --git a/common/oh-package.json5 b/common/oh-package.json5 new file mode 100644 index 0000000..f2d0749 --- /dev/null +++ b/common/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "name": "@ohos/common", + "version": "0.0.1", + "description": "通用模块,用于公共页面、工具或者请求的模块。", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": {} +} \ No newline at end of file diff --git a/common/src/main/ets/constants/AppConstants.ets b/common/src/main/ets/constants/AppConstants.ets new file mode 100644 index 0000000..9a1eb5b --- /dev/null +++ b/common/src/main/ets/constants/AppConstants.ets @@ -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' diff --git a/common/src/main/ets/constants/CommonConstants.ets b/common/src/main/ets/constants/CommonConstants.ets new file mode 100644 index 0000000..ccc6ffc --- /dev/null +++ b/common/src/main/ets/constants/CommonConstants.ets @@ -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' + +}; \ No newline at end of file diff --git a/common/src/main/ets/constants/EventConstants.ets b/common/src/main/ets/constants/EventConstants.ets new file mode 100644 index 0000000..2ec5d1c --- /dev/null +++ b/common/src/main/ets/constants/EventConstants.ets @@ -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; diff --git a/common/src/main/ets/dialog/AgreementDialog.ets b/common/src/main/ets/dialog/AgreementDialog.ets new file mode 100644 index 0000000..94be30f --- /dev/null +++ b/common/src/main/ets/dialog/AgreementDialog.ets @@ -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); +} \ No newline at end of file diff --git a/common/src/main/ets/dialog/GlobalDownloadingDialog.ets b/common/src/main/ets/dialog/GlobalDownloadingDialog.ets new file mode 100644 index 0000000..95f1d80 --- /dev/null +++ b/common/src/main/ets/dialog/GlobalDownloadingDialog.ets @@ -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) + } +} diff --git a/common/src/main/ets/dialog/TipsDialog.ets b/common/src/main/ets/dialog/TipsDialog.ets new file mode 100644 index 0000000..4188ee1 --- /dev/null +++ b/common/src/main/ets/dialog/TipsDialog.ets @@ -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) // 较大的圆角 + } +} diff --git a/common/src/main/ets/dialog/ToastDeleteUtils.ets b/common/src/main/ets/dialog/ToastDeleteUtils.ets new file mode 100644 index 0000000..4344f7e --- /dev/null +++ b/common/src/main/ets/dialog/ToastDeleteUtils.ets @@ -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 | 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); + }); + } +} diff --git a/common/src/main/ets/dialog/ToastUtils.ets b/common/src/main/ets/dialog/ToastUtils.ets new file mode 100644 index 0000000..672f7a4 --- /dev/null +++ b/common/src/main/ets/dialog/ToastUtils.ets @@ -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 | 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; // 可选 +} \ No newline at end of file diff --git a/common/src/main/ets/dialog/UpdateTipsDialog.ets b/common/src/main/ets/dialog/UpdateTipsDialog.ets new file mode 100644 index 0000000..0784269 --- /dev/null +++ b/common/src/main/ets/dialog/UpdateTipsDialog.ets @@ -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) // 较大的圆角 + } +} diff --git a/common/src/main/ets/provider/ApiManager.ets b/common/src/main/ets/provider/ApiManager.ets new file mode 100644 index 0000000..d6234ad --- /dev/null +++ b/common/src/main/ets/provider/ApiManager.ets @@ -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( + url: string, + params?: Record, + method: string = 'get', + data?: Object + ): Promise> { + const service = this.getUnsafeService(); + + const response: AxiosResponse> = 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( + url: string, + params?: Record, + mapper?: (raw: Record) => T, + method: string = 'get', // 新增:默认为 get + data?: Object // 新增:用于 POST 的 Body 数据 + ): Promise { + const service = this.getUnsafeService(); + + // 1. 使用通用的 request 方法,根据参数自动切换 GET/POST + const response: AxiosResponse> = 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; + + // 3. 逻辑转换 + if (mapper !== undefined) { + return mapper(rawData); + } + + // 4. 强转逻辑 + return rawData as T; + } + + /** + * 文件上传方法 + * @param url 请求路径 + * @param formData FormData 实例 + * @param params Query 参数 (用于签名) + */ + public async upload( + url: string, + formData: FormData, + params?: Record + ): Promise { + 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> = await service.post(url, formData, config); + const rawData = (response.data.data ?? {}) as Record; + return rawData as T; + } + + private initialize(): void { + const isDebug: boolean = AppUtil.debug(); + const baseUrl: string = isDebug ? DEBUG_BASE_URL : RELEASE_BASE_URL; + + // 1. 初始化标准实例 + if (this.retrofit === null) { + this.retrofit = axios.create({ + baseURL: baseUrl, + timeout: 10000 + }); + // 基础请求拦截器 + this.retrofit.interceptors.request.use(this.requestInterceptor); + } + + // 2. 初始化不安全实例 + if (this.unsafeRetrofit === null) { + this.unsafeRetrofit = axios.create({ + baseURL: baseUrl, + timeout: 60000, + // 这里的 data 类型使用 Object, headers 使用 AxiosRequestHeaders + transformRequest: [(data: Object, headers: AxiosRequestHeaders): Object => { + // 1. 判断是否为 FormData + if (data instanceof FormData) { + headers.set('Content-Type', 'multipart/form-data'); + return data; + } + + // 2. 判断是否为普通对象 (非空) + if (data !== null && typeof data === 'object') { + headers.set('Content-Type', 'application/json'); + return JSON.stringify(data); + } + + // 3. 其他情况原样返回 (转为 Object 兼容类型) + return data; + }] + }); + + + // --- 添加请求拦截器链 (注意执行顺序:从下往上/链式触发) --- + + // 添加 Header 拦截器 (注入 Token, 设备信息等) + this.unsafeRetrofit.interceptors.request.use(HeaderInterceptor); + + // 添加 RequestInterceptor (处理签名、UUID、Nonce、MD5 日志) + // 注意:因为 onPrepare 是 async,Axios 会自动等待它执行完 + this.unsafeRetrofit.interceptors.request.use( + (config: InternalAxiosRequestConfig): Promise => { + return RequestInterceptor.onPrepare(config as EncryptConfig); + }, + (error: AxiosError): Promise => Promise.reject(error) + ); + + // --- 添加响应拦截器链 --- + + // 添加 ResponseInterceptor (处理解密、状态码修正、响应日志) + this.unsafeRetrofit.interceptors.response.use( + (response: AxiosResponse): Promise => { + // 先执行解密逻辑 + return ResponseInterceptor.responseHandler(response); + }, + (error: AxiosError): Promise => { + 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 { + // 错误处理逻辑 + return Promise.reject(error); + } + + // --- 获取 Service 实例 --- + + /** + * 获取不安全/特殊配置的请求实例 + * 在 ArkTS 中直接返回 AxiosInstance 进行调用 + */ + public getUnsafeService(): AxiosInstance { + if (this.unsafeRetrofit === null) { + this.initialize(); + } + return this.unsafeRetrofit as AxiosInstance; + } +} diff --git a/common/src/main/ets/provider/BaseResponse.ets b/common/src/main/ets/provider/BaseResponse.ets new file mode 100644 index 0000000..d79212c --- /dev/null +++ b/common/src/main/ets/provider/BaseResponse.ets @@ -0,0 +1,14 @@ +// 返回结构:基础返回结构 +export interface BaseResponse { + code: number; + message: string; + data: T; + encrypt: boolean; // 对应你 ResponseInterceptor 中的加密标识 +} +// 返回结构:用于文件上传 +export interface UploadResponse { + status: boolean; + code: number; + message: string; + data: string; +} \ No newline at end of file diff --git a/common/src/main/ets/provider/Callback.ets b/common/src/main/ets/provider/Callback.ets new file mode 100644 index 0000000..cbf6e4b --- /dev/null +++ b/common/src/main/ets/provider/Callback.ets @@ -0,0 +1,5 @@ +export interface RequestCallback { + onSuccess?: (data: T) => void; // 修改这里:接收一个参数 + onFailure?: (message:string) => void; + onError?: (err: Error) => void; +} \ No newline at end of file diff --git a/common/src/main/ets/provider/HeaderInterceptor.ets b/common/src/main/ets/provider/HeaderInterceptor.ets new file mode 100644 index 0000000..fd29edf --- /dev/null +++ b/common/src/main/ets/provider/HeaderInterceptor.ets @@ -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 => { + 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; +} diff --git a/common/src/main/ets/provider/RequestInterceptor.ets b/common/src/main/ets/provider/RequestInterceptor.ets new file mode 100644 index 0000000..e401cb6 --- /dev/null +++ b/common/src/main/ets/provider/RequestInterceptor.ets @@ -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; +} + +export class RequestInterceptor { + static readonly TAG: string = "RabbitLog_Request"; + + static async onPrepare(config: EncryptConfig): Promise { + 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); + } +} diff --git a/common/src/main/ets/provider/ResponseInterceptor.ets b/common/src/main/ets/provider/ResponseInterceptor.ets new file mode 100644 index 0000000..30beacc --- /dev/null +++ b/common/src/main/ets/provider/ResponseInterceptor.ets @@ -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 { + 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 = { + '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); + } +} diff --git a/common/src/main/ets/router/RouterManager.ets b/common/src/main/ets/router/RouterManager.ets new file mode 100644 index 0000000..00f7138 --- /dev/null +++ b/common/src/main/ets/router/RouterManager.ets @@ -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' }); +} + diff --git a/common/src/main/ets/utils/AppUtils.ets b/common/src/main/ets/utils/AppUtils.ets new file mode 100644 index 0000000..efc4aa6 --- /dev/null +++ b/common/src/main/ets/utils/AppUtils.ets @@ -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{ + 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 '未知版本'; + } +} diff --git a/common/src/main/ets/utils/BreakpointSystem.ets b/common/src/main/ets/utils/BreakpointSystem.ets new file mode 100644 index 0000000..49f1a16 --- /dev/null +++ b/common/src/main/ets/utils/BreakpointSystem.ets @@ -0,0 +1,74 @@ +import { mediaquery } from '@kit.ArkUI'; +import { commonConstants } from '../constants/CommonConstants'; + +declare interface BreakPointTypeOption extends Record { + sm?: T; + md?: T; + lg?: T; + xl?: T; + xxl?: T; +} + +export class BreakPointType { + options: BreakPointTypeOption; + + constructor(option: BreakPointTypeOption) { + 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(this.breakpointId, this.currentBreakpoint); + } + } +} \ No newline at end of file diff --git a/common/src/main/ets/utils/DownloaderUtils.ets b/common/src/main/ets/utils/DownloaderUtils.ets new file mode 100644 index 0000000..b60760b --- /dev/null +++ b/common/src/main/ets/utils/DownloaderUtils.ets @@ -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); + } +} + + diff --git a/common/src/main/ets/utils/ImageUtils.ets b/common/src/main/ets/utils/ImageUtils.ets new file mode 100644 index 0000000..2061d3b --- /dev/null +++ b/common/src/main/ets/utils/ImageUtils.ets @@ -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 { + 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 { + 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 { + 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 { + 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 = ` + + +`; + + + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + } + } + +} diff --git a/common/src/main/ets/utils/KVManager.ets b/common/src/main/ets/utils/KVManager.ets new file mode 100644 index 0000000..dd6bba9 --- /dev/null +++ b/common/src/main/ets/utils/KVManager.ets @@ -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{ + 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 { + 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 { + // 注意: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 { + 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 { + return await KVStore.getInstance().get(WX_AUTH_CODE, '') +} \ No newline at end of file diff --git a/common/src/main/ets/utils/KVStore.ets b/common/src/main/ets/utils/KVStore.ets new file mode 100644 index 0000000..1711979 --- /dev/null +++ b/common/src/main/ets/utils/KVStore.ets @@ -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 { + 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(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 { + 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(key: string, defaultValue: T): Promise { + 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(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 { + if (this.pref) { + await this.pref.delete(key); + await this.pref.flush(); + } + } +} diff --git a/common/src/main/ets/utils/Logger.ets b/common/src/main/ets/utils/Logger.ets new file mode 100644 index 0000000..adb73b1 --- /dev/null +++ b/common/src/main/ets/utils/Logger.ets @@ -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}`) + } +} \ No newline at end of file diff --git a/common/src/main/ets/utils/StringUtils.ets b/common/src/main/ets/utils/StringUtils.ets new file mode 100644 index 0000000..e2ad63e --- /dev/null +++ b/common/src/main/ets/utils/StringUtils.ets @@ -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; + } + +} diff --git a/common/src/main/ets/utils/UniMpManager.ets b/common/src/main/ets/utils/UniMpManager.ets new file mode 100644 index 0000000..5397b74 --- /dev/null +++ b/common/src/main/ets/utils/UniMpManager.ets @@ -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>{ + let extraData: Record = {}; + + 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 +} diff --git a/common/src/main/ets/view/SplashWxUniMpView.ets b/common/src/main/ets/view/SplashWxUniMpView.ets new file mode 100644 index 0000000..1b742b2 --- /dev/null +++ b/common/src/main/ets/view/SplashWxUniMpView.ets @@ -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%') + } +} \ No newline at end of file diff --git a/common/src/main/ets/viewmodel/CommonService.ets b/common/src/main/ets/viewmodel/CommonService.ets new file mode 100644 index 0000000..e54bd7a --- /dev/null +++ b/common/src/main/ets/viewmodel/CommonService.ets @@ -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 { + const osVersion: number = deviceInfo.sdkApiVersion; + const oaid: string = AppStorage.get(DEVICE_OAID)??''; + const imei: string = DeviceUtil.getDeviceId(); + + Logger.info('UserService', + ` 请求参数:----> oaid:${oaid}---> osVersion:${osVersion}---> imei:${imei}`); + const queryParams: Record = { + 'oaid': oaid, + 'os_version': osVersion, + "imei": imei, + "cid": '' + }; + let result = await ApiManager.getInstance().requestForMapper( + '/api/user/config', + queryParams, // 传参,如有则传入 + UserProfileMapper // 映射,数据映射器 + ); + AppStorage.setOrCreate(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 = { + "phone": phone + }; + + const result = await ApiManager.getInstance().request( + '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 = { + "timestamp": captchaTimestamp, // 确保这个 value 是 string 或 number + "phone": phone, + "code": captchaCode + }; + const postData: Record = { + "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, callback?: RequestCallback) { + try { + let result = await ApiManager.getInstance().request( + '/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 { + const result = await ApiManager.getInstance().request( + '/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 = { + "code": authCode, + "code_type": '' + }; + const postData: Record = { + "login_type": "weixin", + "weixin": jsonWxAuth + }; + + CommonService.login(postData, callback); + } catch (err) { + if (callback && callback.onError) { + callback.onError(err as Error); + } + } + } + + /** + * 获取支付宝登录参数 + */ + static async getAlipayLoginParams(): Promise { + const result = await ApiManager.getInstance().request( + '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() + 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 = { + "auth_code": authCode + }; + const postData: Record = { + "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) + } +} \ No newline at end of file diff --git a/common/src/main/ets/viewmodel/DataBean.ets b/common/src/main/ets/viewmodel/DataBean.ets new file mode 100644 index 0000000..de0d760 --- /dev/null +++ b/common/src/main/ets/viewmodel/DataBean.ets @@ -0,0 +1,153 @@ +import ArrayList from "@ohos.util.ArrayList"; + +/** + * UserProfile 的专属转换器 + */ +export const UserProfileMapper = (raw: Record): 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) + }; +}; + +/** + * 配置转换器 + */ +export const ConfigMapper = (raw: Record): 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 | 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; // 是否可用 +} \ No newline at end of file diff --git a/common/src/main/ets/viewmodel/LocalBean.ets b/common/src/main/ets/viewmodel/LocalBean.ets new file mode 100644 index 0000000..b48fb09 --- /dev/null +++ b/common/src/main/ets/viewmodel/LocalBean.ets @@ -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 +} + diff --git a/common/src/main/ets/viewmodel/ParamBean.ets b/common/src/main/ets/viewmodel/ParamBean.ets new file mode 100644 index 0000000..b2ffff4 --- /dev/null +++ b/common/src/main/ets/viewmodel/ParamBean.ets @@ -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 +} diff --git a/common/src/main/module.json5 b/common/src/main/module.json5 new file mode 100644 index 0000000..d682b53 --- /dev/null +++ b/common/src/main/module.json5 @@ -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" + } + } + ], + } +} diff --git a/common/src/main/resources/base/element/string.json b/common/src/main/resources/base/element/string.json new file mode 100644 index 0000000..283e923 --- /dev/null +++ b/common/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "page_show", + "value": "page from package" + }, + { + "name": "reason_read_image", + "value": "我们需要读取您的相册以选择图片完成拼图。" + }, + { + "name": "reason_oaid", + "value": "用于创建唯一的oaid服务标识。" + } + ] +} diff --git a/common/src/main/resources/base/media/ic_dialog_tips_mask_top.png b/common/src/main/resources/base/media/ic_dialog_tips_mask_top.png new file mode 100644 index 0000000..f498841 Binary files /dev/null and b/common/src/main/resources/base/media/ic_dialog_tips_mask_top.png differ diff --git a/common/src/main/resources/base/media/ic_splash_alipay_unimp.webp b/common/src/main/resources/base/media/ic_splash_alipay_unimp.webp new file mode 100644 index 0000000..9b6e53f Binary files /dev/null and b/common/src/main/resources/base/media/ic_splash_alipay_unimp.webp differ diff --git a/common/src/main/resources/base/media/ic_splash_wx_unimp.webp b/common/src/main/resources/base/media/ic_splash_wx_unimp.webp new file mode 100644 index 0000000..d928679 Binary files /dev/null and b/common/src/main/resources/base/media/ic_splash_wx_unimp.webp differ diff --git a/common/src/main/resources/base/media/ic_toast_delete_account.png b/common/src/main/resources/base/media/ic_toast_delete_account.png new file mode 100644 index 0000000..16c5e56 Binary files /dev/null and b/common/src/main/resources/base/media/ic_toast_delete_account.png differ diff --git a/common/src/test/List.test.ets b/common/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/common/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/common/src/test/LocalUnit.test.ets b/common/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/common/src/test/LocalUnit.test.ets @@ -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); + }); + }); +} \ No newline at end of file diff --git a/features/mainLayout/BuildProfile.ets b/features/mainLayout/BuildProfile.ets new file mode 100644 index 0000000..bdb642c --- /dev/null +++ b/features/mainLayout/BuildProfile.ets @@ -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; +} \ No newline at end of file diff --git a/features/mainLayout/Index.ets b/features/mainLayout/Index.ets new file mode 100644 index 0000000..0276833 --- /dev/null +++ b/features/mainLayout/Index.ets @@ -0,0 +1 @@ +export { FeedbackLayout } from './src/main/ets/pages/FeedbackLayout'; \ No newline at end of file diff --git a/features/mainLayout/build-profile.json5 b/features/mainLayout/build-profile.json5 new file mode 100644 index 0000000..515bbf0 --- /dev/null +++ b/features/mainLayout/build-profile.json5 @@ -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" + } + ] +} \ No newline at end of file diff --git a/features/mainLayout/consumer-rules.txt b/features/mainLayout/consumer-rules.txt new file mode 100644 index 0000000..e69de29 diff --git a/features/mainLayout/hvigorfile.ts b/features/mainLayout/hvigorfile.ts new file mode 100644 index 0000000..805c5d7 --- /dev/null +++ b/features/mainLayout/hvigorfile.ts @@ -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. */ +} \ No newline at end of file diff --git a/features/mainLayout/obfuscation-rules.txt b/features/mainLayout/obfuscation-rules.txt new file mode 100644 index 0000000..1e7e54e --- /dev/null +++ b/features/mainLayout/obfuscation-rules.txt @@ -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 \ No newline at end of file diff --git a/features/mainLayout/oh-package-lock.json5 b/features/mainLayout/oh-package-lock.json5 new file mode 100644 index 0000000..40bd91f --- /dev/null +++ b/features/mainLayout/oh-package-lock.json5 @@ -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" + } + } +} \ No newline at end of file diff --git a/features/mainLayout/oh-package.json5 b/features/mainLayout/oh-package.json5 new file mode 100644 index 0000000..4fb86d8 --- /dev/null +++ b/features/mainLayout/oh-package.json5 @@ -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", + } +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/BuildProfile.ets b/features/mainLayout/oh_modules/@ohos/common/BuildProfile.ets new file mode 100644 index 0000000..a35b214 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/BuildProfile.ets @@ -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; +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/Index.ets b/features/mainLayout/oh_modules/@ohos/common/Index.ets new file mode 100644 index 0000000..cfb6552 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/Index.ets @@ -0,0 +1,3 @@ +export { Logger } from './src/main/ets/utils/Logger'; + +export { BreakpointSystem, BreakPointType } from './src/main/ets/utils/BreakpointSystem'; \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/build-profile.json5 b/features/mainLayout/oh_modules/@ohos/common/build-profile.json5 new file mode 100644 index 0000000..4f2a475 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/build-profile.json5 @@ -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" + } + ] +} diff --git a/features/mainLayout/oh_modules/@ohos/common/consumer-rules.txt b/features/mainLayout/oh_modules/@ohos/common/consumer-rules.txt new file mode 100644 index 0000000..e69de29 diff --git a/features/mainLayout/oh_modules/@ohos/common/hvigorfile.ts b/features/mainLayout/oh_modules/@ohos/common/hvigorfile.ts new file mode 100644 index 0000000..805c5d7 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/hvigorfile.ts @@ -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. */ +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/obfuscation-rules.txt b/features/mainLayout/oh_modules/@ohos/common/obfuscation-rules.txt new file mode 100644 index 0000000..1e7e54e --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/obfuscation-rules.txt @@ -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 \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/oh-package.json5 b/features/mainLayout/oh_modules/@ohos/common/oh-package.json5 new file mode 100644 index 0000000..f2d0749 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "name": "@ohos/common", + "version": "0.0.1", + "description": "通用模块,用于公共页面、工具或者请求的模块。", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": {} +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/AppConstants.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/AppConstants.ets new file mode 100644 index 0000000..9a1eb5b --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/AppConstants.ets @@ -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' diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/CommonConstants.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/CommonConstants.ets new file mode 100644 index 0000000..ccc6ffc --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/CommonConstants.ets @@ -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' + +}; \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/EventConstants.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/EventConstants.ets new file mode 100644 index 0000000..2ec5d1c --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/constants/EventConstants.ets @@ -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; diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/AgreementDialog.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/AgreementDialog.ets new file mode 100644 index 0000000..94be30f --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/AgreementDialog.ets @@ -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); +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/GlobalDownloadingDialog.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/GlobalDownloadingDialog.ets new file mode 100644 index 0000000..95f1d80 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/GlobalDownloadingDialog.ets @@ -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) + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/TipsDialog.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/TipsDialog.ets new file mode 100644 index 0000000..4188ee1 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/TipsDialog.ets @@ -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) // 较大的圆角 + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/ToastDeleteUtils.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/ToastDeleteUtils.ets new file mode 100644 index 0000000..4344f7e --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/ToastDeleteUtils.ets @@ -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 | 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); + }); + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/ToastUtils.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/ToastUtils.ets new file mode 100644 index 0000000..672f7a4 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/ToastUtils.ets @@ -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 | 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; // 可选 +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/UpdateTipsDialog.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/UpdateTipsDialog.ets new file mode 100644 index 0000000..0784269 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/dialog/UpdateTipsDialog.ets @@ -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) // 较大的圆角 + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/ApiManager.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/ApiManager.ets new file mode 100644 index 0000000..d6234ad --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/ApiManager.ets @@ -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( + url: string, + params?: Record, + method: string = 'get', + data?: Object + ): Promise> { + const service = this.getUnsafeService(); + + const response: AxiosResponse> = 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( + url: string, + params?: Record, + mapper?: (raw: Record) => T, + method: string = 'get', // 新增:默认为 get + data?: Object // 新增:用于 POST 的 Body 数据 + ): Promise { + const service = this.getUnsafeService(); + + // 1. 使用通用的 request 方法,根据参数自动切换 GET/POST + const response: AxiosResponse> = 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; + + // 3. 逻辑转换 + if (mapper !== undefined) { + return mapper(rawData); + } + + // 4. 强转逻辑 + return rawData as T; + } + + /** + * 文件上传方法 + * @param url 请求路径 + * @param formData FormData 实例 + * @param params Query 参数 (用于签名) + */ + public async upload( + url: string, + formData: FormData, + params?: Record + ): Promise { + 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> = await service.post(url, formData, config); + const rawData = (response.data.data ?? {}) as Record; + return rawData as T; + } + + private initialize(): void { + const isDebug: boolean = AppUtil.debug(); + const baseUrl: string = isDebug ? DEBUG_BASE_URL : RELEASE_BASE_URL; + + // 1. 初始化标准实例 + if (this.retrofit === null) { + this.retrofit = axios.create({ + baseURL: baseUrl, + timeout: 10000 + }); + // 基础请求拦截器 + this.retrofit.interceptors.request.use(this.requestInterceptor); + } + + // 2. 初始化不安全实例 + if (this.unsafeRetrofit === null) { + this.unsafeRetrofit = axios.create({ + baseURL: baseUrl, + timeout: 60000, + // 这里的 data 类型使用 Object, headers 使用 AxiosRequestHeaders + transformRequest: [(data: Object, headers: AxiosRequestHeaders): Object => { + // 1. 判断是否为 FormData + if (data instanceof FormData) { + headers.set('Content-Type', 'multipart/form-data'); + return data; + } + + // 2. 判断是否为普通对象 (非空) + if (data !== null && typeof data === 'object') { + headers.set('Content-Type', 'application/json'); + return JSON.stringify(data); + } + + // 3. 其他情况原样返回 (转为 Object 兼容类型) + return data; + }] + }); + + + // --- 添加请求拦截器链 (注意执行顺序:从下往上/链式触发) --- + + // 添加 Header 拦截器 (注入 Token, 设备信息等) + this.unsafeRetrofit.interceptors.request.use(HeaderInterceptor); + + // 添加 RequestInterceptor (处理签名、UUID、Nonce、MD5 日志) + // 注意:因为 onPrepare 是 async,Axios 会自动等待它执行完 + this.unsafeRetrofit.interceptors.request.use( + (config: InternalAxiosRequestConfig): Promise => { + return RequestInterceptor.onPrepare(config as EncryptConfig); + }, + (error: AxiosError): Promise => Promise.reject(error) + ); + + // --- 添加响应拦截器链 --- + + // 添加 ResponseInterceptor (处理解密、状态码修正、响应日志) + this.unsafeRetrofit.interceptors.response.use( + (response: AxiosResponse): Promise => { + // 先执行解密逻辑 + return ResponseInterceptor.responseHandler(response); + }, + (error: AxiosError): Promise => { + 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 { + // 错误处理逻辑 + return Promise.reject(error); + } + + // --- 获取 Service 实例 --- + + /** + * 获取不安全/特殊配置的请求实例 + * 在 ArkTS 中直接返回 AxiosInstance 进行调用 + */ + public getUnsafeService(): AxiosInstance { + if (this.unsafeRetrofit === null) { + this.initialize(); + } + return this.unsafeRetrofit as AxiosInstance; + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/BaseResponse.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/BaseResponse.ets new file mode 100644 index 0000000..d79212c --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/BaseResponse.ets @@ -0,0 +1,14 @@ +// 返回结构:基础返回结构 +export interface BaseResponse { + code: number; + message: string; + data: T; + encrypt: boolean; // 对应你 ResponseInterceptor 中的加密标识 +} +// 返回结构:用于文件上传 +export interface UploadResponse { + status: boolean; + code: number; + message: string; + data: string; +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/Callback.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/Callback.ets new file mode 100644 index 0000000..cbf6e4b --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/Callback.ets @@ -0,0 +1,5 @@ +export interface RequestCallback { + onSuccess?: (data: T) => void; // 修改这里:接收一个参数 + onFailure?: (message:string) => void; + onError?: (err: Error) => void; +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/HeaderInterceptor.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/HeaderInterceptor.ets new file mode 100644 index 0000000..fd29edf --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/HeaderInterceptor.ets @@ -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 => { + 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; +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/RequestInterceptor.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/RequestInterceptor.ets new file mode 100644 index 0000000..e401cb6 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/RequestInterceptor.ets @@ -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; +} + +export class RequestInterceptor { + static readonly TAG: string = "RabbitLog_Request"; + + static async onPrepare(config: EncryptConfig): Promise { + 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); + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/ResponseInterceptor.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/ResponseInterceptor.ets new file mode 100644 index 0000000..30beacc --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/provider/ResponseInterceptor.ets @@ -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 { + 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 = { + '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); + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/router/RouterManager.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/router/RouterManager.ets new file mode 100644 index 0000000..00f7138 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/router/RouterManager.ets @@ -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' }); +} + diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/AppUtils.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/AppUtils.ets new file mode 100644 index 0000000..efc4aa6 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/AppUtils.ets @@ -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{ + 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 '未知版本'; + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/BreakpointSystem.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/BreakpointSystem.ets new file mode 100644 index 0000000..49f1a16 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/BreakpointSystem.ets @@ -0,0 +1,74 @@ +import { mediaquery } from '@kit.ArkUI'; +import { commonConstants } from '../constants/CommonConstants'; + +declare interface BreakPointTypeOption extends Record { + sm?: T; + md?: T; + lg?: T; + xl?: T; + xxl?: T; +} + +export class BreakPointType { + options: BreakPointTypeOption; + + constructor(option: BreakPointTypeOption) { + 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(this.breakpointId, this.currentBreakpoint); + } + } +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/DownloaderUtils.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/DownloaderUtils.ets new file mode 100644 index 0000000..b60760b --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/DownloaderUtils.ets @@ -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); + } +} + + diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/ImageUtils.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/ImageUtils.ets new file mode 100644 index 0000000..2061d3b --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/ImageUtils.ets @@ -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 { + 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 { + 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 { + 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 { + 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 = ` + + +`; + + + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + } + } + +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/KVManager.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/KVManager.ets new file mode 100644 index 0000000..dd6bba9 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/KVManager.ets @@ -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{ + 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 { + 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 { + // 注意: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 { + 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 { + return await KVStore.getInstance().get(WX_AUTH_CODE, '') +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/KVStore.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/KVStore.ets new file mode 100644 index 0000000..1711979 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/KVStore.ets @@ -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 { + 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(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 { + 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(key: string, defaultValue: T): Promise { + 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(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 { + if (this.pref) { + await this.pref.delete(key); + await this.pref.flush(); + } + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/Logger.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/Logger.ets new file mode 100644 index 0000000..adb73b1 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/Logger.ets @@ -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}`) + } +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/StringUtils.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/StringUtils.ets new file mode 100644 index 0000000..e2ad63e --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/StringUtils.ets @@ -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; + } + +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/UniMpManager.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/UniMpManager.ets new file mode 100644 index 0000000..5397b74 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/utils/UniMpManager.ets @@ -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>{ + let extraData: Record = {}; + + 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 +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/view/SplashWxUniMpView.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/view/SplashWxUniMpView.ets new file mode 100644 index 0000000..1b742b2 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/view/SplashWxUniMpView.ets @@ -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%') + } +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/CommonService.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/CommonService.ets new file mode 100644 index 0000000..e54bd7a --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/CommonService.ets @@ -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 { + const osVersion: number = deviceInfo.sdkApiVersion; + const oaid: string = AppStorage.get(DEVICE_OAID)??''; + const imei: string = DeviceUtil.getDeviceId(); + + Logger.info('UserService', + ` 请求参数:----> oaid:${oaid}---> osVersion:${osVersion}---> imei:${imei}`); + const queryParams: Record = { + 'oaid': oaid, + 'os_version': osVersion, + "imei": imei, + "cid": '' + }; + let result = await ApiManager.getInstance().requestForMapper( + '/api/user/config', + queryParams, // 传参,如有则传入 + UserProfileMapper // 映射,数据映射器 + ); + AppStorage.setOrCreate(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 = { + "phone": phone + }; + + const result = await ApiManager.getInstance().request( + '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 = { + "timestamp": captchaTimestamp, // 确保这个 value 是 string 或 number + "phone": phone, + "code": captchaCode + }; + const postData: Record = { + "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, callback?: RequestCallback) { + try { + let result = await ApiManager.getInstance().request( + '/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 { + const result = await ApiManager.getInstance().request( + '/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 = { + "code": authCode, + "code_type": '' + }; + const postData: Record = { + "login_type": "weixin", + "weixin": jsonWxAuth + }; + + CommonService.login(postData, callback); + } catch (err) { + if (callback && callback.onError) { + callback.onError(err as Error); + } + } + } + + /** + * 获取支付宝登录参数 + */ + static async getAlipayLoginParams(): Promise { + const result = await ApiManager.getInstance().request( + '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() + 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 = { + "auth_code": authCode + }; + const postData: Record = { + "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) + } +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/DataBean.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/DataBean.ets new file mode 100644 index 0000000..de0d760 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/DataBean.ets @@ -0,0 +1,153 @@ +import ArrayList from "@ohos.util.ArrayList"; + +/** + * UserProfile 的专属转换器 + */ +export const UserProfileMapper = (raw: Record): 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) + }; +}; + +/** + * 配置转换器 + */ +export const ConfigMapper = (raw: Record): 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 | 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; // 是否可用 +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/LocalBean.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/LocalBean.ets new file mode 100644 index 0000000..b48fb09 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/LocalBean.ets @@ -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 +} + diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/ParamBean.ets b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/ParamBean.ets new file mode 100644 index 0000000..b2ffff4 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/ets/viewmodel/ParamBean.ets @@ -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 +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/module.json5 b/features/mainLayout/oh_modules/@ohos/common/src/main/module.json5 new file mode 100644 index 0000000..d682b53 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/module.json5 @@ -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" + } + } + ], + } +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/element/string.json b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/element/string.json new file mode 100644 index 0000000..283e923 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "page_show", + "value": "page from package" + }, + { + "name": "reason_read_image", + "value": "我们需要读取您的相册以选择图片完成拼图。" + }, + { + "name": "reason_oaid", + "value": "用于创建唯一的oaid服务标识。" + } + ] +} diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_dialog_tips_mask_top.png b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_dialog_tips_mask_top.png new file mode 100644 index 0000000..f498841 Binary files /dev/null and b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_dialog_tips_mask_top.png differ diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_splash_alipay_unimp.webp b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_splash_alipay_unimp.webp new file mode 100644 index 0000000..9b6e53f Binary files /dev/null and b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_splash_alipay_unimp.webp differ diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_splash_wx_unimp.webp b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_splash_wx_unimp.webp new file mode 100644 index 0000000..d928679 Binary files /dev/null and b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_splash_wx_unimp.webp differ diff --git a/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_toast_delete_account.png b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_toast_delete_account.png new file mode 100644 index 0000000..16c5e56 Binary files /dev/null and b/features/mainLayout/oh_modules/@ohos/common/src/main/resources/base/media/ic_toast_delete_account.png differ diff --git a/features/mainLayout/oh_modules/@ohos/common/src/test/List.test.ets b/features/mainLayout/oh_modules/@ohos/common/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/features/mainLayout/oh_modules/@ohos/common/src/test/LocalUnit.test.ets b/features/mainLayout/oh_modules/@ohos/common/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/features/mainLayout/oh_modules/@ohos/common/src/test/LocalUnit.test.ets @@ -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); + }); + }); +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/constants/CommonConstants.ets b/features/mainLayout/src/main/ets/constants/CommonConstants.ets new file mode 100644 index 0000000..e3a2160 --- /dev/null +++ b/features/mainLayout/src/main/ets/constants/CommonConstants.ets @@ -0,0 +1,220 @@ +interface CommonConstantsInterface { + breakpointsSmName: string; + breakpointsMdName: string; + breakpointsLgName: string; + breakpointsXlName: string; + tabSmVertical: boolean; + tabMdVertical: boolean; + tabLgVertical: boolean; + tabXlVertical: boolean; + tabSmBarWidth: string; + tabMdBarWidth: string; + tabLgBarWidth: string; + tabXlBarWidth: string; + tabSmBarHeight: string; + tabMdBarHeight: string; + tabLgBarHeight: string; + tabXlBarHeight: string; + tabContentWidth: string; + tabContentHeight: string; + flexTabBarWidth: string; + flexTabBarHeight: string; + firstTabChooseIndex: number; + columnHeight: string; + tabSize: number; + gridSize: number; + breakpointsInitializeName: string; + columnAspectRatio: number; + columnTextFontWeight: number; + rowWidth: string; + smColTemplate: string; + mdColTemplate: string; + lgColTemplate: string; + xlColTemplate: string; + initializeTemplate: string; + breakpointSystemName: string; + systemRowWidth: 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 value of tab vertical in sm device. + */ + tabSmVertical: false, + + /** + * The value of tab vertical in md device. + */ + tabMdVertical: false, + + /** + * The value of tab vertical in lg device. + */ + tabLgVertical: true, + + /** + * The value of tab vertical in xl device. + */ + tabXlVertical: true, + + /** + * The percentage of width of tab bar in sm device. + */ + tabSmBarWidth: '100%', + + /** + * The percentage of width of tab bar in md device. + */ + tabMdBarWidth: '100%', + + /** + * The width of tab bar in lg device. + */ + tabLgBarWidth: '96vp', + + /** + * The width of tab bar in xl device. + */ + tabXlBarWidth: '96vp', + + /** + * The height of tab bar in sm device. + */ + tabSmBarHeight: '56vp', + + /** + * The height of tab bar in md device. + */ + tabMdBarHeight: '56vp', + + /** + * The percentage of height tab bar in lg device. + */ + tabLgBarHeight: '60%', + + /** + * The percentage of height tab bar in xl device. + */ + tabXlBarHeight: '60%', + + /** + * The percentage of width of tab content. + */ + tabContentWidth: '100%', + + /** + * The percentage of height of tab content. + */ + tabContentHeight: '100%', + + /** + * The percentage of width of flex component. + */ + flexTabBarWidth: '100%', + + /** + * The percentage of height of flex component. + */ + flexTabBarHeight: '80%', + + /** + * The percentage of height in row component. + */ + firstTabChooseIndex: 0, + + /** + * The percentage of height in row component. + */ + columnHeight: '100%', + + /** + * The number of tab component. + */ + tabSize: 2, + + /** + * The number of grid item. + */ + gridSize: 8, + + /** + * Initialize device breakpoints. + */ + breakpointsInitializeName: 'md', + + /** + * The value of aspect ratio in column component. + */ + columnAspectRatio: 16 / 9, + + /** + * Font weight of text component. + */ + columnTextFontWeight: 400, + + /** + * The percentage of width of row component. + */ + rowWidth: '100%', + + /** + * The value of attribute of columnsTemplate in sm device. + */ + smColTemplate: '1fr 1fr', + + /** + * The value of attribute of columnsTemplate in md device. + */ + mdColTemplate: '1fr 1fr 1fr', + + /** + * The value of attribute of columnsTemplate in lg device. + */ + lgColTemplate: '1fr 1fr 1fr 1fr', + + /** + * The value of attribute of columnsTemplate in xl device. + */ + xlColTemplate: '1fr 1fr 1fr 1fr', + + /** + * Initialization of attribute columnsTemplate. + */ + initializeTemplate: '1fr 1fr', + + /** + * Breakpoint name. + */ + breakpointSystemName: 'mainBreakpoint', + + + /** + * The percentage of width of row component. + */ + systemRowWidth: '100%', + + +}; \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/pages/AboutUsLayout.ets b/features/mainLayout/src/main/ets/pages/AboutUsLayout.ets new file mode 100644 index 0000000..e5d0415 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/AboutUsLayout.ets @@ -0,0 +1,183 @@ +import { DemoItem } from '../viewmodel/LocalBean'; +import { WebParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { AGREEMENT_URL, PRIVACY_URL } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { TitleBarComponent } from '../view/TitleBarComponent'; + +@Builder +export function AboutUsBuilder() { + AboutUsLayout() +} + +/** + * 设置页面 + */ +@Component +export struct AboutUsLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + + build() { + NavDestination() { + Stack() { + Column() { + //标题栏 + TitleBarComponent({titleName:'关于我们', isShowTitle:true, isShowBack:true}) + + Column(){ + Image($r('app.media.ic_app_logo')) + .width('100vp') + .height('100vp') + .margin({ top: 120 }) + .align(Alignment.Center) + + Column() { + Row() { + Text('用户协议') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8}) + } + .width('100%') + .height('52vp') + .padding({ left: 14, right: 14}) + .onClick(() => { + // 跳转内置WebView(用户协议) + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: AGREEMENT_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Text('隐私政策') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8}) + } + .width('100%') + .height('52vp') + .padding({ left: 14, right: 14}) + .onClick(() => { + // 跳转内置WebView(隐私政策) + const params: WebParams = { + id: 1001, + name: "隐私政策", + type: 1, + url: PRIVACY_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + } + .backgroundColor(Color.White) + .width('100%') + .borderRadius(12) + .margin({ top: 30}) + .padding({ top: 6, bottom: 6}) + } + .padding({ left: 16, right: 16}) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#FFF9F9F9') + } + .hideTitleBar(true) + } +} + +/** + * 账号 item + */ +@Component +export struct AccountItem { + @State isChecked: boolean = false; + onChange: (checked: boolean) => void = () => {}; + + item: DemoItem | null = null; + + build() { + Row() { + Stack(){ + Image(this.item?.icon) + .width('100%') + .height('100%') + .onClick(() => { + }) + } + .width('60vp') + .height('60vp') + .margin({ left: 11, right: 11, top: 16, bottom: 16}) + .alignContent(Alignment.Center) + .backgroundColor('#FFF3F3F3') + .borderRadius(8) + + Column(){ + Text(this.item?.name) + .width('100%') + .layoutWeight(1) + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(FontWeight.Medium) + + Text('ID:123456') + .width('100%') + .fontSize(12) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .layoutWeight(1) + .margin({ top: 16, bottom: 16 }) + + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(20) + .height(20) + .margin({ right: 16 }) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + } + .width('100%') + .height('80vp') + .backgroundColor('#FFFFFFFF') + .borderRadius(8) + .shadow({ + radius: 5, // 阴影模糊半径(数值越大越模糊) + color: '#4DE5E5E5', // 阴影颜色(建议带透明度,如 10% 透明的黑色) + offsetX: 0, // X 轴偏移 + offsetY: 0 // Y 轴偏移(正数向下,负数向上) + }) + .margin({ bottom: 16 }) + } +} + diff --git a/features/mainLayout/src/main/ets/pages/AccountBindLayout.ets b/features/mainLayout/src/main/ets/pages/AccountBindLayout.ets new file mode 100644 index 0000000..fde8c48 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/AccountBindLayout.ets @@ -0,0 +1,239 @@ +import { USER_INFO } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { TipsDialog } from '@ohos/common/src/main/ets/dialog/TipsDialog'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { KVStore } from '@ohos/common/src/main/ets/utils/KVStore'; +import { CommonService } from '@ohos/common/src/main/ets/viewmodel/CommonService'; +import { UserInfo } from '@ohos/common/src/main/ets/viewmodel/DataBean'; +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { MainService } from '../viewmodel/MainService'; +import { BindParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { getWechatAuthCode } from '@ohos/common/src/main/ets/utils/KVManager'; + +@Builder +export function AccountBindBuilder() { + AccountBindLayout() +} + +/** + * 设置页面 + */ +@Component +export struct AccountBindLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + private unBindType: number = 0;//0:Normal 1:phone 2:微信 + @State currentUserInfo: UserInfo | undefined = undefined; + + dialogController: CustomDialogController = new CustomDialogController({ + builder: TipsDialog({ + title: this.unBindType===2 ? '确定解除绑定的微信?' : '解开绑定的手机号?', + content: this.unBindType===2 ? '解除后将无法使用该微信登录此账号,请谨慎操作!' : '当前绑定的手机号码为', + desc: this.unBindType===2 ? '' : '+86 ' + this.currentUserInfo?.phone?.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'), + cancel: () => { console.info('用户点击取消') }, + confirm: () => { + if(this.unBindType===1){ + // 解绑请求 + MainService.unBindPhone(this.currentUserInfo?.phone || '',{ + onSuccess: (_) => { + UnBindSuccess() + //添加1秒延迟,防止数据未写入 + setTimeout(() => { + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + }) + }, 1000); + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '解绑失败'); + } + }) + }else{ + // 解绑微信 + getWechatAuthCode().then(wechatCode => { + // 微信解绑请求 + MainService.unBindWechat(wechatCode,{ + onSuccess: (_) => { + UnBindSuccess() + //添加1秒延迟,防止数据未写入 + setTimeout(() => { + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + }) + }, 1000); + } + }) + }) + } + this.unBindType = 0 + } + }), + alignment: DialogAlignment.Center, // 居中显示 + customStyle: true, // 使用自定义样式,去掉系统默认背景 + autoCancel: true // 点击遮罩层可关闭 + }) + + aboutToAppear() { + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + }) + } + + build() { + NavDestination() { + Stack() { + Scroll(){ + Column() { + //标题栏 + TitleBarComponent({titleName:'账号绑定', isShowTitle:true, isShowBack:true}) + + // 内容区 + Column(){ + Column() { + Row() { + Text('手机号') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + + Row(){ + if(this.currentUserInfo?.phone && !this.currentUserInfo?.temp){ + Text('+86') + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + Text(this.currentUserInfo?.phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')) + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + .margin({ left: 8 }) + }else{ + Text('去绑定') + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + .margin({ left: 8 }) + } + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + if(this.currentUserInfo?.phone && !this.currentUserInfo?.temp){ + // 解绑,弹出确认窗 + this.unBindType = 1 + this.dialogController.open() + }else{ + // 绑定手机号 + const params: BindParams = { + id: 1001, + name: "手机", + type: 1 + }; + this.pageStack.pushPathByName("AccountBindLoginLayout", params); + } + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Text('微信账号') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + + Row(){ + Text(this.currentUserInfo?.weixinAppId && !this.currentUserInfo?.temp ? '已绑定' : '去绑定') + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + // 绑定微信 + if(this.currentUserInfo?.weixinAppId && !this.currentUserInfo?.temp){ + // 解绑,弹出确认窗 + this.unBindType = 2 + this.dialogController.open() + }else{ + // 绑定微信 + const params: BindParams = { + id: 1002, + name: "微信", + type: 2 + }; + this.pageStack.pushPathByName("AccountBindLoginLayout", params); + } + }) + + } + .backgroundColor(Color.White) + .width('100%') + .borderRadius(12) + .margin({ top: 30}) + } + .padding({ left: 16, right: 16}) + } + .width('100%') + .height('100%') + } + .layoutWeight(1) + .scrollBar(BarState.Off) + } + .width('100%') + .height('100%') + .alignContent(Alignment.Bottom) + .backgroundColor('#FFF9F9F9') + } + .hideTitleBar(true) + .onShown(() => { + // 上一页返回时,刷新账号数据(添加1秒延迟,防止数据未写入) + setTimeout(() => { + let result = AppStorage.get>('back'); + if (result && result.isBack && result.source === 'bindLogin') { + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + }) + AppStorage.setOrCreate('back', null); // 用完即删,防止重复触发 + } + }, 1000); + }) + } +} + +function UnBindSuccess() { + ToastUtils.normalToast({ + message: '已解除绑定', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + + //重新获取用户配置(数据会持久化到本地) + CommonService.getUserProfile().then(_ => { + // 当用户配置获取完成后,重新获取用户信息(数据会持久化到本地) + CommonService.getUserInfo() + }) + +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/pages/AccountBindLoginLayout.ets b/features/mainLayout/src/main/ets/pages/AccountBindLoginLayout.ets new file mode 100644 index 0000000..dcd4633 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/AccountBindLoginLayout.ets @@ -0,0 +1,583 @@ +import { BindParams, WebParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { AGREEMENT_URL, IS_AGREE_AGREEMENT, PRIVACY_URL, + WX_BIND_AUTHOR } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { KVStore } from '@ohos/common/src/main/ets/utils/KVStore'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { promptAction } from '@kit.ArkUI'; +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { MainService } from '../viewmodel/MainService'; +import { CommonService } from '@ohos/common/src/main/ets/viewmodel/CommonService'; +import { common } from '@kit.AbilityKit'; +import { emitter } from '@kit.BasicServicesKit'; +import { EVT_BIND_WX_AUTHOR_SUCCESS } from '@ohos/common/src/main/ets/constants/EventConstants'; + +@Builder +export function AccountBindLoginBuilder() { + AccountBindLoginLayout() +} + +/** + * 绑定账号页面(类似登录) + * 与登录页相比:只显示单一的绑定方式 + */ +@Component +struct AccountBindLoginLayout { + @Consume('pageInfos') pageStack: NavPathStack; + @State fromSource: string = ''; + @State hasAgreed?: boolean = undefined; + @State bindParams: BindParams | null = null; + + /* + // 在 Page 页面中 + dialogController: CustomDialogController = new CustomDialogController({ + builder: AgreementDialog({ + confirm: () => { + // 执行你的逻辑 + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + } + }), + // 1. 禁止点击空白处关闭 + autoCancel: false, + + // 2. 拦截返回手势 (API 11+) 如果用户触发返回手势,此回调会被触发。 不调用 dismiss() 则弹窗不会消失。 + onWillDismiss: (dismissDialogAction: DismissDialogAction) => { + console.info("Reason: " + JSON.stringify(dismissDialogAction.reason)); + // 如果是因为点击遮罩(0)或按下返回键(1)触发,我们不做处理,从而实现禁止关闭 + if (dismissDialogAction.reason === DismissReason.PRESS_BACK || + dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) { + } + }, + alignment: DialogAlignment.Center, + customStyle: true + }) + */ + + async aboutToAppear() { + emitter.once({ eventId: EVT_BIND_WX_AUTHOR_SUCCESS }, (eventData: emitter.EventData) => { + const payload = eventData.data; + if (payload && payload["result"]) { + const authCode: string = payload["result"] as string; + + // 获取授权码,请求登录 + MainService.wxBind(authCode, { + onSuccess: (_) => { + BindSuccess(this.pageStack) + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '绑定失败'); + } + }) + } + }); + } + + build() { + NavDestination() { + Stack() { + // 1. 背景层 + Stack(){ + Image($r('app.media.ic_login_mask')).width('100%') + } + .width('100%') + .height('100%') + .alignContent(Alignment.TopStart) + + // 2. 主体内容层 (标题 + 绑定框) + Column() { + TitleBarComponent({ + titleName: '', + isShowTitle: false, + isShowBack: true + }) + //显示绑定内容 + if(this.bindParams?.type === 2){ + // 微信绑定 + BindForWechat({isChecked: this.hasAgreed}) + }else{ + //一键绑定OR手机绑定 + BindForPhone({isChecked: this.hasAgreed}) + } + } + .width('100%') + .height('100%') + + /* + // 3. 底部“其他方式”层 + Column() { + Row() { + Image($r('app.media.ic_login_other_left_line')) + .margin({ right: 12 }) + .width(60) + .height(2) + + Text('其他方式') + .fontSize(16) + .fontColor('#FF767676') + .fontWeight(FontWeight.Bold) + + Image($r('app.media.ic_login_other_right_line')) + .margin({ left: 12 }) + .width(60) + .height(2) + } + .alignItems(VerticalAlign.Center) // 确保横线和文字垂直居中对齐 + + Row(){ + Image($r('app.media.ic_login_wx_icon')) + .width(40) + .height(40) + .onClick(() => { + this.loginMethod = 2; + }) + + Image($r('app.media.ic_login_phone_icon')) + .width(40) + .height(40) + .margin({ left: 18 }) + .onClick(() => { + this.loginMethod = 0; + }) + } + .margin({ top: 27, bottom: 60 }) + } + .width('100%') + .margin({ bottom: 40 }) // 距离屏幕底部留一点间距,避免贴边 + */ + } + .alignContent(Alignment.Bottom) // 将 Stack 默认对齐方式设为底部,或者手动控制层级 + .width('100%') + .height('100%') + .backgroundColor($r('sys.color.white')) + } + .onReady((context: NavDestinationContext) => { + this.bindParams = context.pathInfo.param as BindParams; + }) + .hideTitleBar(true) + } +} + +@Component +struct BindForPhone { + @Consume('pageInfos') pageStack: NavPathStack; + @State textPhoneValue: string = ''; + @State textCaptchaValue: string = ''; + @State captchaTimestampValue: string = ''; + + @State countdown: number = 0; // 倒计时秒数 + @State isCounting: boolean = false; // 是否正在倒计时 + private timerId: number = -1; // 定时器ID + + @Prop isChecked: boolean = false; + onChange: (checked: boolean) => void = (checked: boolean) => { + if(checked){ 'true' }else{ 'false' } + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + }; + + cancel: () => void = () => {}; + confirm: () => void = () => { + if(!this.textPhoneValue){ + ToastUtils.showToast(this.getUIContext(), '请输入手机号码') + }else if(!this.isChecked){ + ToastUtils.showToast(this.getUIContext(), '请先同意隐私和协议...') + }else if(!this.textCaptchaValue){ + ToastUtils.showToast(this.getUIContext(), '请输入验证码') + }else{ + MainService.bindForCaptcha(this.textPhoneValue, this.textCaptchaValue, this.captchaTimestampValue, { + onSuccess: (_) => { + BindSuccess(this.pageStack) + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '绑定失败'); + } + }) + } + }; + + + build() { + Column() { + // Logo图标区域 + Row() { + Image($r('app.media.ic_login_vertical_line')) + .width('4vp') + .height('55vp') + + Column(){ + Text('欢迎使用') + .width('100%') + .fontSize(18) + .fontColor('#FF000000') + .textAlign(TextAlign.Start) + .fontWeight(FontWeight.Bold) + + Text('截图兔') + .width('100%') + .fontSize(32) + .fontColor('#FF000000') + .textAlign(TextAlign.Start) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .margin({left: 12}) + + } + .width('100%') + .justifyContent(FlexAlign.Start) + .margin({top: 63}) + + //账号输入框 + Stack({ alignContent: Alignment.BottomEnd }) { + Row(){ + Text('+86') + .fontSize(14) + .fontColor('#FF3D3D3D') + .textAlign(TextAlign.Start) + + Image($r('app.media.ic_login_input_div_line')) + .width(1) + .height(28) + .margin({ left: 12 }) + + TextInput({ placeholder: '请输入手机号', text: this.textPhoneValue }) + .width('100%') + .type(InputType.PhoneNumber) + .maxLength(11) + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .placeholderColor('#FF767676') + .backgroundColor('#00000000') + .borderRadius(8) + .borderColor('#00000000') + .borderWidth(0.5) + .onChange((v) => this.textPhoneValue = v) + } + } + .width('100%') + .backgroundColor('#FFF6F6F6') + .borderRadius(8) + .borderColor('#FFDEDEDE') + .borderWidth(0.5) + .margin({top: 100}) + .padding({ left: 16, right: 16}) + + //验证码输入框 + Stack({ alignContent: Alignment.BottomEnd }) { + Row(){ + TextInput({ placeholder: '请输入验证码', text: this.textCaptchaValue }) + .layoutWeight(1) + .type(InputType.Number) + .maxLength(20) + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .placeholderColor('#FF767676') + .backgroundColor('#00000000') + .borderRadius(8) + .borderColor('#00000000') + .borderWidth(0.5) + .onChange((v) => this.textCaptchaValue = v) + Stack() { + Text(this.isCounting ? `${this.countdown}s后重新获取` : '获取验证码') + .fontSize(12) + .fontColor(this.isCounting ? '#FF999999' : '#FF3D3D3D') // 倒计时变灰 + .textAlign(TextAlign.Start) + } + .backgroundColor('#FFFFFFFF') + .borderRadius(8) + .borderColor('#00000000') + .borderWidth(0.5) + .padding({ left: 19, right: 19, top: 12, bottom: 12}) + .onClick(() => { + if (this.isCounting) return; // 倒计时中点击无效 + + if (!this.textPhoneValue || this.textPhoneValue.length < 11) { + ToastUtils.showToast(this.getUIContext(), '请输入正确的手机号码'); + return; + } + + CommonService.sendSmsCode(this.textPhoneValue,{ + onSuccess: (captchaTimestamp) => { + this.captchaTimestampValue = captchaTimestamp.toString() + // 发送成功后调用倒计时 + this.startCountdown(); + + ToastUtils.normalToast({ + message: '验证码已发送', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '发送失败'); + } + }) + + }) + + } + } + .width('100%') + .backgroundColor('#FFF6F6F6') + .borderRadius(12) + .borderColor('#FFDEDEDE') + .borderWidth(0.5) + .margin({top: 20}) + .padding({ left: 14, right: 5}) + + //绑定按钮 + Button('绑定') + .onClick(() => { + this.confirm(); + }) + .backgroundColor(Color.Black) // 对应图片中的黑色按钮 + .fontColor('#FFC2FF43') + .borderRadius(359) + .fontSize(16) + .height(46) + .width('100%') + .margin({top: 46, bottom:14}) + + //用户协议 + Row(){ + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(16) + .height(16) + .margin({ right: 4 }) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + // 添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + + Text() { + Span('我已阅读并同意') + .fontColor('#80aaaaaa') + + Span('《用户协议》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: AGREEMENT_URL + }; + this.pageStack.pushPathByName("WebLayout", params) + }) + + Span('和') + .fontColor('#80aaaaaa') + + Span('《隐私政策》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: PRIVACY_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + } + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ right: 4 }) + + } + } + .width('100%') + .height('100%') + .padding({ left: 38, right: 38}) + } + + // 开启倒计时 + startCountdown() { + if (this.isCounting) return; + + this.countdown = 60; + this.isCounting = true; + + // 清理可能存在的旧定时器 + clearInterval(this.timerId); + + // 使用箭头函数确保 this 指向 LoginForPhone 组件 + this.timerId = setInterval(() => { + if (this.countdown > 1) { + this.countdown--; + } else { + this.stopCountdown(); + } + }, 1000); + } + + + // 停止倒计时 + stopCountdown() { + this.isCounting = false; + this.countdown = 0; + clearInterval(this.timerId); + } + + // 组件销毁前清理,防止内存泄漏 + aboutToDisappear() { + this.stopCountdown(); + } +} + +@Component +struct BindForWechat { + @Consume('pageInfos') pageStack: NavPathStack; + + @Prop isChecked: boolean = false; + onChange: (checked: boolean) => void = (checked: boolean) => { + if(checked){ 'true' }else{ 'false' } + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + }; + build() { + Column(){ + Image($r('app.media.ic_app_logo')) + .width('100vp') + .height('100vp') + .margin({ top: 120 }) + .align(Alignment.Center) + + Text('截图兔') + .fontSize(16) + .fontColor('#FF000000') + .textAlign(TextAlign.Center) + .fontWeight(FontWeight.Bold) + .margin({ top: 8 }) + + Column() { + Text('为了更好地为您提供服务,请先完成微信授权') + .fontSize(14) + .fontColor('#805d5c5c') + .textAlign(TextAlign.Center) + .margin({ top: 30 }) + + Row() { + Image($r('app.media.ic_login_wx_auth_icon')) + .width(16) + .height(16) + .margin({ right: 4 }) + + Text('微信授权绑定') + .fontSize(16) + .fontColor('#FFC2FF43') + .textAlign(TextAlign.Center) + } + .width('100%') + .height(48) + .backgroundColor('#FF252525') + .borderRadius(359) + .margin({ top: 8 }) + .padding({ top: 6, bottom: 6 }) + .align(Alignment.Center) + .justifyContent(FlexAlign.Center) + .onClick(() => { + if (this.isChecked) { + AppStorage.setOrCreate(WX_BIND_AUTHOR, 2); + CommonService.wxAuthorize(getContext(this) as common.UIAbilityContext, true); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先同意隐私和协议...') + } + }) + } + .margin({ bottom:14 }) + + //用户协议 + Row(){ + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(16) + .height(16) + .margin({ right: 4 }) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + + Text() { + Span('我已阅读并同意') + .fontColor('#80aaaaaa') + + Span('《用户协议》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: AGREEMENT_URL + }; + this.pageStack.pushPathByName("WebLayout", params) + }) + + Span('和') + .fontColor('#80aaaaaa') + + Span('《隐私政策》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: PRIVACY_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + } + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ right: 4 }) + + } + } + .width('100%') + .height('100%') + .padding({ left: 16, right: 16}) + } +} + +function BindSuccess(pageStack: NavPathStack) { + ToastUtils.normalToast({ + message: '绑定成功', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + + //重新获取用户配置(数据会持久化到本地) + CommonService.getUserProfile().then(_ => { + // 当用户配置获取完成后,重新获取用户信息(数据会持久化到本地) + CommonService.getUserInfo() + }) + + AppStorage.setOrCreate('back', { isBack: true, source: 'bindLogin' }); + pageStack.pop(); +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/pages/AccountManagerLayout.ets b/features/mainLayout/src/main/ets/pages/AccountManagerLayout.ets new file mode 100644 index 0000000..86df3d4 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/AccountManagerLayout.ets @@ -0,0 +1,280 @@ +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { MainService } from '../viewmodel/MainService'; +import { KVStore } from '@ohos/common/src/main/ets/utils/KVStore'; +import { USER_INFO } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { routerLogin } from '@ohos/common/src/main/ets/router/RouterManager'; +import { UserInfo } from '@ohos/common/src/main/ets/viewmodel/DataBean'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { TipsDialog } from '@ohos/common/src/main/ets/dialog/TipsDialog'; +import { CommonService } from '@ohos/common/src/main/ets/viewmodel/CommonService'; + +@Builder +export function AccountManagerBuilder() { + AccountManagerLayout() +} + +/** + * 设置页面 + */ +@Component +export struct AccountManagerLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + + @State currentUserInfo: UserInfo | undefined = undefined; + @State accountList: Array|undefined = undefined + @State selectedAccount: UserInfo | undefined = undefined; + + dialogController: CustomDialogController = new CustomDialogController({ + builder: TipsDialog({ + title: '您确定要切换账户吗?', + content: '', + desc: '', + cancel: () => { console.info('用户点击取消') }, + confirm: () => { + if (!this.selectedAccount) { + ToastUtils.normalToast({ + message: '出错啦~', // 提示文字 + bottom: '50%' + }); + return; + } + MainService.switchAccount(this.selectedAccount.user_id??'',{ + onSuccess: (_) => { + ToastUtils.normalToast({ + message: '切换成功', // 提示文字 + bottom: '50%' + }); + + //重新获取用户配置(数据会持久化到本地) + setTimeout(() => { + this.syncData() + }, 1000); + } + }) + } + }), + alignment: DialogAlignment.Center, // 居中显示 + customStyle: true, // 使用自定义样式,去掉系统默认背景 + autoCancel: true // 点击遮罩层可关闭 + }) + + async syncData() { + await CommonService.getUserProfile() + await CommonService.getUserInfo() + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + }) + MainService.getAccount().then(res => { + this.accountList = res; + }); + } + + aboutToAppear() { + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + }) + MainService.getAccount().then(res => { + this.accountList = res; + }); + } + + build() { + NavDestination() { + Stack() { + Image($r('app.media.ic_mine_account_mask')) + .width('100%') + .height('100%') + .backgroundColor('#FFF9F9F9') + + Column() { + //标题栏 + TitleBarComponent({titleName:'', isShowTitle:false, isShowBack:true}) + + Stack (){ + List(){ + // 标题栏 + ListItem() { + Text('轻触头像以切换帐号') + .width('100%') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Center) + .borderRadius(12) + .fontWeight(1) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 55 }) + } + + ForEach(this.accountList, (item: UserInfo) => { + ListItem() { + AccountItem({ + currentUserInfo: this.currentUserInfo, + item: item, + isChecked: item.user_id === this.currentUserInfo?.user_id, + onChange: (_: boolean) => { + // 切换帐号 + this.selectedAccount = item; + this.dialogController.open() + } + }) + } + }) + + // 添加帐号栏 + ListItem() { + Row() { + Stack(){ + Image($r('app.media.ic_image_add')) + .width('100%') + .height('100%') + } + .width('60vp') + .height('60vp') + .margin({ left: 11, right: 11, top: 16, bottom: 16}) + .alignContent(Alignment.Center) + .backgroundColor('#FFF3F3F3') + .borderRadius(8) + + Text('添加帐号') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(FontWeight.Bold) + .layoutWeight(1) + } + .width('100%') + .height('80vp') + .margin({ bottom: 46 }) + .backgroundColor('#FFFFFFFF') + .borderRadius(8) + .shadow({ + radius: 5, // 阴影模糊半径(数值越大越模糊) + color: '#4DE5E5E5', // 阴影颜色(建议带透明度,如 10% 透明的黑色) + offsetX: 0, // X 轴偏移 + offsetY: 0 // Y 轴偏移(正数向下,负数向上) + }) + .onClick(() => { + routerLogin('account',false) + }) + } + } + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.None) + } + .width('100%') + .layoutWeight(1) + .alignContent(Alignment.Center) + .padding({ left: 16, right: 16}) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + } + .hideTitleBar(true) + .onShown(() => { + // 上一页返回时,刷新账号数据 + let result = AppStorage.get>('back'); + if (result && result.isBack && result.source === 'login') { + MainService.getAccount().then(res => { + + setTimeout(() => { + // 延迟刷新当前登录用户信息,否则可能因为数据未刷新而显示错误 + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.currentUserInfo = JSON.parse(userJson) as UserInfo; + this.accountList = res; + }) + }, 800); + }); + AppStorage.setOrCreate('back', null); // 用完即删,防止重复触发 + } + }) + } +} + +/** + * 账号 item + */ +@Component +export struct AccountItem { + @State currentUserInfo: UserInfo | undefined = undefined; + @State item: UserInfo | null = null; + @State isChecked: boolean = false; + onChange: (checked: boolean) => void = () => {}; + + build() { + Row() { + Stack(){ + Image(this.item?.avater || $r('app.media.ic_mine_user_normal')) + .width('100%') + .height('100%') + .borderRadius(8) + .onClick(() => { + }) + } + .width('60vp') + .height('60vp') + .margin({ left: 11, right: 11, top: 16, bottom: 16}) + .alignContent(Alignment.Center) + .backgroundColor('#FFF3F3F3') + .borderRadius(8) + + Column(){ + Text(this.item?.name || '大白兔') + .width('100%') + .layoutWeight(1) + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(FontWeight.Medium) + + Text('ID: ' + this.item?.user_id) + .width('100%') + .fontSize(12) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .layoutWeight(1) + .margin({ top: 16, bottom: 16 }) + + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(20) + .height(20) + .margin({ right: 16 }) + .objectFit(ImageFit.Contain) + // 添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + } + .width('100%') + .height('80vp') + .backgroundColor('#FFFFFFFF') + .borderRadius(8) + .shadow({ + radius: 5, // 阴影模糊半径(数值越大越模糊) + color: '#4DE5E5E5', // 阴影颜色(建议带透明度,如 10% 透明的黑色) + offsetX: 0, // X 轴偏移 + offsetY: 0 // Y 轴偏移(正数向下,负数向上) + }) + .margin({ bottom: 16 }) + .onClick(() => { + if(this.isChecked && this.item?.user_id === this.currentUserInfo?.user_id) { + ToastUtils.showToast(this.getUIContext(), '当前帐号已登录'); + return + }; + //this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + } +} + diff --git a/features/mainLayout/src/main/ets/pages/CuteLayout.ets b/features/mainLayout/src/main/ets/pages/CuteLayout.ets new file mode 100644 index 0000000..3736780 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/CuteLayout.ets @@ -0,0 +1,1001 @@ +import { CuteParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { ResizeStyle, TrimStyle } from '../viewmodel/LocalBean'; +import { image } from '@kit.ImageKit'; +import { StickerView } from '../view/StickerView'; +import { common } from '@kit.AbilityKit'; +import { Logger } from '@ohos/common'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { ImageUtils } from '@ohos/common/src/main/ets/utils/ImageUtils'; +import cuteViewModel from "../viewmodel/CuteViewModel"; +import { StickerItem } from '@ohos/common/src/main/ets/viewmodel/LocalBean'; + +@Builder +export function CuteBuilder() { + CuteLayout() +} + +/** + * 抠图页面 + */ +@Component +export struct CuteLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + @State cuteParams: CuteParams | null = null; + + @State stickers: StickerItem[] = []; + @State selectedIndex: number = -1; // 当前选中的图片索引 + + @State backgroundColorVal: string | null = '#FFFFFFFF'; + @State selectedImageUri:string | null = null; + @State processedPixelMap: image.PixelMap | null = null; + + //服装和发现预览按钮展示 + @State isPreviewButtonShow: boolean = false; + @State clothingShow: boolean = true; + @State hairStyleShow: boolean = true; + + + + + aboutToAppear(): void { + ImageUtils.openCutoutGallery().then(item => { + if (item) { + this.processedPixelMap = item.pixelMap; + cuteViewModel.setProcessedPixelMap(this.processedPixelMap); + let newItem = new StickerItem(this.processedPixelMap,0); + newItem.zIndex = 10; + this.stickers.push(newItem); + this.selectedIndex = this.stickers.length - 1; + } + }) + } + + build() { + NavDestination() { + Column() { + TitleBarComponent({ + titleName: '', + isShowTitle: false, + isShowBack: true, + isShowSave: true, + onSave: () => { + ImageUtils.saveImageForID('sticker_container') + } + }) + + Stack() { + Stack({alignContent:Alignment.Top}){ + if(this.processedPixelMap == null){ + Stack() { + Column(){ + Image($r('app.media.ic_image_pld')).width(120).height(111.2) + Text('去相册选一张美照吧~').fontColor('#FFD8D8D8').fontSize(12) + } + } + .width('80%') + .aspectRatio((this.cuteParams?.width??300) / (this.cuteParams?.height??420))//300/420 + .backgroundColor('#FFFFFFFF') + .borderColor('#FFD8D8D8') + .borderWidth(1) + .onClick(() => { + ImageUtils.openCutoutGallery().then(item => { + if (item) { + this.processedPixelMap = item.pixelMap; + cuteViewModel.setProcessedPixelMap(this.processedPixelMap); + let newItem = new StickerItem(this.processedPixelMap,0); + newItem.zIndex = 10; + this.stickers.push(newItem); + this.selectedIndex = this.stickers.length - 1; + } + }) + }) + + }else{ + Stack() { + Rect() + .fill(this.backgroundColorVal) + .width('100%') + .height('100%') + .zIndex(-1) + + ForEach(this.stickers, (item: StickerItem, index: number) => { + StickerView({ + item: item, + selectedIndex: $selectedIndex, // 使用 $ 实现双向绑定 + index: index + }) + }, (item: StickerItem) => item.id) + + } + .id('sticker_container') + .width('80%') + .aspectRatio((this.cuteParams?.width??300) / (this.cuteParams?.height??420)) + .clip(true) + } + } + .width('100%') + .height('100%') + + + Column(){ + //预览按钮(服装/发型) + if(this.isPreviewButtonShow){ + Row(){ + Row(){ + Image($r(this.clothingShow ? 'app.media.ic_look' : 'app.media.ic_un_look')).width('14vp').height('14vp').margin({ left: 5, top: 5, bottom: 5 }) + Text('服装').fontSize(14).fontColor(this.clothingShow ?'#FF1A1A1A':'#FF767676').margin({ left: 4, right: 5, top: 5, bottom: 5 }) + } + .borderRadius(1008) + .borderWidth(1) + .borderColor(this.clothingShow ?'#FF000000':'#00000000') + .backgroundColor(this.clothingShow ?'#00000000':'#FFFFFF') + .onClick(()=>{ + this.clothingShow = !this.clothingShow; + let index = this.stickers.findIndex(item => item.type === 1); + if (index !== -1) { + this.stickers[index].isHidden = !this.clothingShow; + } + }) + + Row(){ + Image($r(this.hairStyleShow ? 'app.media.ic_look' : 'app.media.ic_un_look')).width('14vp').height('14vp').margin({ left: 5, top: 5, bottom: 5 }) + Text('发型').fontSize(14).fontColor(this.hairStyleShow ?'#FF1A1A1A':'#FF767676').margin({ left: 4, right: 5, top: 5, bottom: 5 }) + } + .borderRadius(1008) + .borderWidth(1) + .borderColor(this.hairStyleShow ?'#FF000000':'#00000000') + .backgroundColor(this.hairStyleShow ?'#00000000':'#FFFFFF') + .margin({ left: 8 }) + .onClick(()=>{ + this.hairStyleShow = !this.hairStyleShow; + let index = this.stickers.findIndex(item => item.type === 2); + if (index !== -1) { + this.stickers[index].isHidden = !this.hairStyleShow; + } + }) + } + .width('100%') + .margin({ left: 16, bottom: 12 }) + .justifyContent(FlexAlign.Start) + } + BottomToolBar({ + onSwitch: async (_: string, type: number) => { + if( type === 1 || type === 2 ){ + this.isPreviewButtonShow = true; + }else{ + this.isPreviewButtonShow = false; + } + }, + onParamChange: async (name: string, type: number, color: string | undefined, clothing: TrimStyle | undefined, hairStyle: TrimStyle | undefined, width: number, height:number) => { + const context = getContext(this) as common.UIAbilityContext; + const defaultIcon = $r('app.media.ic_cute_background_color_normal'); + //type:0 颜色,1 衣服, 2 发型, 3 尺寸 + if(type==0){ + this.backgroundColorVal = color??'#FFFFFFFF' + }else if(type==1){ + let clothingIcon = clothing?.icon ?? $r('app.media.ic_cute_background_color_normal'); + if (clothing?.icon.id == defaultIcon.id) { + let tempStickers = this.stickers.filter(s => s.type !== 1); + + this.stickers = tempStickers; + this.selectedIndex = this.stickers.length - 1; + }else{ + try { + const pm = await ImageUtils.getPixelMapFromResource(clothingIcon, context); + + if (pm) { + let clothingItem = this.stickers.find(item => {return item.type === 1}); + let newItem = new StickerItem(pm,1); + newItem.zIndex = 20; + if(this.stickers.length<=1){ + this.stickers.push(newItem); + }else if (clothingItem) { + clothingItem.pixelMap = pm; + } else { + let tempStickers = this.stickers.filter(s => s.type !== 1); + tempStickers.push(newItem); + + this.stickers = tempStickers; + this.selectedIndex = this.stickers.findIndex(s => s.id === newItem.id); + } + } + } catch (e) { + console.error("加载失败: " + e); + } + } + }else if(type==2){ + let hairStyleIcon = hairStyle?.icon ?? $r('app.media.ic_cute_background_color_normal'); + if (hairStyle?.icon.id == defaultIcon.id) { + //无 + let tempStickers = this.stickers.filter(s => s.type !== 2); + + this.stickers = tempStickers; + this.selectedIndex = this.stickers.length - 1; + }else{ + try { + const pm = await ImageUtils.getPixelMapFromResource(hairStyleIcon, context); + + if (pm) { + let hairStyleItem = this.stickers.find(item => {return item.type === 2}); + + let newItem = new StickerItem(pm,2); + newItem.zIndex = 20; + + if(this.stickers.length<=1){ + //只有头像则直接添加 + this.stickers.push(newItem); + }else if (hairStyleItem) { + hairStyleItem.pixelMap = pm; + } else { + let tempStickers = this.stickers.filter(s => s.type !== 2); + tempStickers.push(newItem); + + this.stickers = tempStickers; + this.selectedIndex = this.stickers.findIndex(s => s.id === newItem.id); + } + } + } catch (e) { + console.error("加载失败: " + e); + } + } + }else if(type==3){ + if (this.cuteParams) { + this.cuteParams.width = width; + this.cuteParams.height = height; + } else { + this.cuteParams = { id: 0, name: name, width: width, height: height }; + } + } + } + }) + .width('100%') + } + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .width('100%') + .height('100%') + .backgroundColor('#FFF4F4F4') + } + .onReady((context: NavDestinationContext) => { + this.cuteParams = context.pathInfo.param as CuteParams; + }) + .hideTitleBar(true) + } +} + + +@Component +struct BottomToolBar { + onSwitch?: (name: string, type: number) => void; + onParamChange?: (name: string, type: number, color: string | undefined, clothing: TrimStyle | undefined, hairStyle: TrimStyle | undefined, width: number, height:number) => void; + @State indexTable: number = 0; + @State clothingShow: boolean = true; + @State hairStyleShow: boolean = false; + + build() { + Column(){ + if(this.indexTable == 0){ + BackgroundColorBar({ + onColorChange: (color: string) => { + this.onParamChange?.('', 0, color, undefined, undefined, 0, 0) + } + }) + .onAppear(() => { + this.onSwitch?.('背景', 0); + }) + } + else if(this.indexTable == 1){ + ClothingBar({ + onClothingChange: (clothing: TrimStyle) => { + Logger.info('BottomToolBar', 'ClothingBar--->onClothingChange'); + this.onParamChange?.('', 1, undefined, clothing, undefined, 0, 0) + } + }) + .onAppear(() => { + this.onSwitch?.('服装', 1); + }) + } + else if(this.indexTable == 2){ + HairstyleBar({ + onHairstyleChange: (hairStyle: TrimStyle) => { + this.onParamChange?.('', 2, undefined, undefined, hairStyle, 0, 0) + } + }) + .onAppear(() => { + this.onSwitch?.('发型', 2); + }) + }else if(this.indexTable == 3){ + ResizeBar({ + onResizeChange: (resizeStyle: ResizeStyle) => { + this.onParamChange?.('', 3, undefined, undefined, undefined, resizeStyle.width, resizeStyle.height) + } + }) + .onAppear(() => { + this.onSwitch?.('尺寸', 3); + }) + } + + // 底部工具tab栏 + Row() { + Row(){ + if(this.indexTable == 0){ + Image($r('app.media.ic_cute_background_icon')).width('14vp').height('14vp').margin({ right: 2}) + } + Text('背景') + .fontSize(14) + .height(34) + .fontColor(this.indexTable == 0? '#FF1A1A1A' : '#FFAAAAAA') + .align(Alignment.Center) + } + .justifyContent(FlexAlign.Center) // 设置主轴(横向)居中 + .alignItems(VerticalAlign.Center) // 设置交叉轴(纵向)居中 + .layoutWeight(1) + .borderRadius(359) + .borderColor(this.indexTable == 0? '#FF1A1A1A' : '#FFAAAAAA') + .borderWidth(1) + .margin({right: 13, bottom: 20}) + .onClick(()=>{ + this.indexTable = 0; + }) + + Row(){ + if(this.indexTable == 1){ + Image($r('app.media.ic_cute_clothing_icon')).width('14vp').height('14vp').margin({ right: 2}) + } + Text('服装') + .fontSize(14) + .height(34) + .fontColor(this.indexTable == 1? '#FF1A1A1A' : '#FFAAAAAA') + .align(Alignment.Center) + } + .justifyContent(FlexAlign.Center) // 设置主轴(横向)居中 + .alignItems(VerticalAlign.Center) // 设置交叉轴(纵向)居中 + .layoutWeight(1) + .borderRadius(359) + .borderColor(this.indexTable == 1? '#FF1A1A1A' : '#FFAAAAAA') + .borderWidth(1) + .margin({right: 13, bottom: 20}) + .onClick(()=>{ + this.indexTable = 1; + }) + + Row(){ + if(this.indexTable == 2) { + Image($r('app.media.ic_cute_hairstyle_icon')).width('14vp').height('14vp').margin({ right: 2}) + } + Text('发型') + .fontSize(14) + .height(34) + .fontColor(this.indexTable == 2? '#FF1A1A1A' : '#FFAAAAAA') + .align(Alignment.Center) + } + .justifyContent(FlexAlign.Center) // 设置主轴(横向)居中 + .alignItems(VerticalAlign.Center) // 设置交叉轴(纵向)居中 + .layoutWeight(1) + .borderRadius(359) + .borderColor(this.indexTable == 2? '#FF1A1A1A' : '#FFAAAAAA') + .borderWidth(1) + .margin({right: 13, bottom: 20}) + .onClick(()=>{ + this.indexTable = 2; + }) + + Row(){ + if(this.indexTable == 3) { + Image($r('app.media.ic_cute_resize_icon')).width('14vp').height('14vp').margin({ right: 2}) + } + Text('尺寸') + .fontSize(14) + .height(34) + .fontColor(this.indexTable == 3? '#FF1A1A1A' : '#FFAAAAAA') + .align(Alignment.Center) + } + .justifyContent(FlexAlign.Center) // 设置主轴(横向)居中 + .alignItems(VerticalAlign.Center) // 设置交叉轴(纵向)居中 + .layoutWeight(1) + .borderRadius(359) + .borderColor(this.indexTable == 3? '#FF1A1A1A' : '#FFAAAAAA') + .borderWidth(1) + .margin({bottom: 20}) + .onClick(()=>{ + this.indexTable = 3; + }) + } + .width('100%') + .padding({left:16, right:16, top:20}) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius({ topLeft: 16, topRight: 16}) + .padding({top: 13}) + } +} + +@Component +struct BackgroundColorBar { + onColorChange?: (color: string) => void; + @State indexSelected: number = 0; + @State colors: Array = ['#FFFF0000', '#FFCA1318', '#FF990C09', '#FF438EDB', '#FF1838EF', '#FF38CBFB', '#FFFFFFFF']; + build() { + Column() { + Text('基础色') + .fontSize(16) + .fontColor('#FF1A1A1A') + + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + // .offset({ y: -18 }) + + Row(){ + Stack() { + Image($r('app.media.ic_cute_background_color_normal')) + .aspectRatio(1) + .layoutWeight(1) + .width('26vp') + .height('26vp') + .onClick(()=>{ + this.indexSelected = 0; + + this.onColorChange?.('#00000000') + }) + if(0 == this.indexSelected){ + Circle() + .width('20vp') + .height('20vp') + .fill(Color.Transparent) + .stroke('#4d414040') + .strokeWidth('2vp') + } + } + + Row().width(23) + ForEach(this.colors, (color: string, index: number) => { + Stack() { + Circle() + .width('26vp') + .height('26vp') + .fill(color) + + if((index + 1) == this.indexSelected){ + Circle() + .width('24vp') + .height('24vp') + .fill(color == '#FFFFFFFF'? '#FFEDEDED' :'#FFFFFFFF') + } + + Circle() + .width('12vp') + .height('12vp') + .fill(color) + } + .width('26vp') + .height('26vp') + .borderWidth(0.5) + .borderColor('#FFAAAAAA') + .borderRadius(359) + .aspectRatio(1) + .layoutWeight(1) + .onClick(()=>{ + this.indexSelected = index + 1; + this.onColorChange?.(color) + }) + if(index < this.colors.length-1){ + Row().width(23) + } + }) + } + .width('100%') + .height(84) + .padding({left: 16, right: 16}) + } + } +} + +@Component +struct ClothingBar { + onClothingChange?: (clothing: TrimStyle) => void; + @State indexSelected: number = 0; + @State femaleSelected: number = 0; + @State manSelected: number = 0; + @State private clothingFemales: TrimStyle[] = [ + { id: '0', type: 0, name: '无', icon: $r('app.media.ic_cute_background_color_normal') }, + { id: '1', type: 0, name: '青春', icon: $r('app.media.ic_clothing_female_1') }, + { id: '2', type: 0, name: '学生', icon: $r('app.media.ic_clothing_female_2') }, + { id: '3', type: 0, name: '职场', icon: $r('app.media.ic_clothing_female_3') }, + { id: '4', type: 0, name: '休闲', icon: $r('app.media.ic_clothing_female_4') }, + { id: '5', type: 0, name: '日韩', icon: $r('app.media.ic_clothing_female_5') }, + { id: '6', type: 0, name: '艺术', icon: $r('app.media.ic_clothing_female_6') } + ]; + @State private clothingMans: TrimStyle[] = [ + { id: '0', type: 1, name: '无', icon: $r('app.media.ic_cute_background_color_normal') }, + { id: '1', type: 1, name: '青春', icon: $r('app.media.ic_clothing_man_2') }, + { id: '2', type: 1, name: '学生', icon: $r('app.media.ic_clothing_man_3') }, + { id: '3', type: 1, name: '职场', icon: $r('app.media.ic_clothing_man_4') }, + { id: '4', type: 1, name: '休闲', icon: $r('app.media.ic_clothing_man_5') }, + { id: '5', type: 1, name: '日韩', icon: $r('app.media.ic_clothing_man_6') }, + { id: '6', type: 1, name: '艺术', icon: $r('app.media.ic_clothing_man_7') } + ]; + build() { + Column() { + //男装~女装 + Row(){ + Column(){ + Text('女装') + .fontSize(16) + .fontColor('#FF1A1A1A') + + if(0 == this.indexSelected){ + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + } + .onClick(()=>{ + this.indexSelected = 0; + }) + .justifyContent(FlexAlign.Center) + .layoutWeight(1) + + Column(){ + Text('男装') + .fontSize(16) + .fontColor('#FF1A1A1A') + + if(1 == this.indexSelected){ + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + } + .onClick(()=>{ + this.indexSelected = 1; + }) + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } + .margin({ bottom:12 }) + + if(0 == this.indexSelected){ + Scroll(){ + Row(){ + ForEach(this.clothingFemales, (clothing: TrimStyle, index: number) => { + Stack() { + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, // 渐变方向:180度表示从上到下 + colors: [ + ['#FFF4F4F4', 0.0], // 起始颜色:灰色,位置 0% + ['#00FFFFFF', 1.0] // 结束颜色:白色透明,位置 100% + ] + }) + Stack({ alignContent: Alignment.Bottom }){ + if(clothing.name == '无'){ + Stack(){ + Image(clothing.icon) + .width(25) + .height(25) + } + .width('100%') + .height('100%') + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.femaleSelected = index; + this.onClothingChange?.(clothing); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + }else{ + Image(clothing.icon) + .width('100%') + .layoutWeight(1) + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.femaleSelected = index; + this.onClothingChange?.(clothing); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + } + Text(clothing.name) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor('#FFFFFFFF') + .width('100%') + .height(16) + .backgroundColor('#4D000000') + .backdropBlur(9.29) + .borderRadius({bottomLeft:3.71, bottomRight:3.71}) + } + .width('100%') + .height('100%') + } + .width(65) + .height(82) + .aspectRatio(65/82) + .borderRadius(3.71) + .borderColor(this.femaleSelected == index? '#FFC2FF43' : '#00000000') + .borderWidth(1.86) + .margin({ left: index==0? 16 : 8, right: index==this.clothingFemales.length-1? 16 : 0}) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + }else{ + Scroll(){ + Row(){ + ForEach(this.clothingMans, (clothing: TrimStyle, index: number) => { + Stack({ alignContent: Alignment.Bottom }) { + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, // 渐变方向:180度表示从上到下 + colors: [ + ['#FFF4F4F4', 0.0], // 起始颜色:灰色,位置 0% + ['#00FFFFFF', 1.0] // 结束颜色:白色透明,位置 100% + ] + }) + if(clothing.name == '无'){ + Stack(){ + Image(clothing.icon) + .width(25) + .height(25) + } + .width('100%') + .height('100%') + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.manSelected = index; + this.onClothingChange?.(clothing); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + }else{ + Image(clothing.icon) + .width('100%') + .layoutWeight(1) + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.manSelected = index; + this.onClothingChange?.(clothing); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + } + Text(clothing.name) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor('#FFFFFFFF') + .width('100%') + .height(16) + .backgroundColor('#4D000000') + .backdropBlur(9.29) + .borderRadius({bottomLeft:3.71, bottomRight:3.71}) + } + .width(65) + .height(82) + .aspectRatio(65/82) + .borderRadius(3.71) + .borderColor(this.manSelected == index? '#FFC2FF43' : '#00000000') + .borderWidth(1.86) + .margin({ left: index==0? 16 : 8, right: index==this.clothingMans.length-1? 16 : 0}) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + } + } + } +} + +@Component +struct HairstyleBar { + onHairstyleChange?: (hairstyle: TrimStyle) => void; + @State indexSelected: number = 0; + @State femaleSelected: number = 0; + @State manSelected: number = 0; + @State private hairstyleFemales: TrimStyle[] = [ + { id: '0', type: 0, name: '无', icon: $r('app.media.ic_cute_background_color_normal') }, + { id: '1', type: 0, name: '青春', icon: $r('app.media.ic_hairstyle_female_1') }, + { id: '2', type: 0, name: '学生', icon: $r('app.media.ic_hairstyle_female_2') }, + { id: '3', type: 0, name: '职场', icon: $r('app.media.ic_hairstyle_female_3') }, + { id: '4', type: 0, name: '休闲', icon: $r('app.media.ic_hairstyle_female_4') }, + { id: '5', type: 0, name: '日韩', icon: $r('app.media.ic_hairstyle_female_5') }, + { id: '6', type: 0, name: '艺术', icon: $r('app.media.ic_hairstyle_female_6') } + ]; + @State private hairstyleMans: TrimStyle[] = [ + { id: '0', type: 1, name: '无', icon: $r('app.media.ic_cute_background_color_normal') }, + { id: '1', type: 1, name: '青春', icon: $r('app.media.ic_hairstyle_man_1') }, + { id: '2', type: 1, name: '学生', icon: $r('app.media.ic_hairstyle_man_2') }, + { id: '3', type: 1, name: '职场', icon: $r('app.media.ic_hairstyle_man_3') }, + { id: '4', type: 1, name: '休闲', icon: $r('app.media.ic_hairstyle_man_4') }, + { id: '5', type: 1, name: '日韩', icon: $r('app.media.ic_hairstyle_man_5') }, + { id: '6', type: 1, name: '艺术', icon: $r('app.media.ic_hairstyle_man_6') } + ]; + build() { + Column() { + //男~女发型 + Row(){ + Column(){ + Text('女生') + .fontSize(16) + .fontColor('#FF1A1A1A') + + if(0 == this.indexSelected){ + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + } + .onClick(()=>{ + this.indexSelected = 0; + }) + .justifyContent(FlexAlign.Center) + .layoutWeight(1) + + Column(){ + Text('男生') + .fontSize(16) + .fontColor('#FF1A1A1A') + + if(1 == this.indexSelected){ + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + } + .onClick(()=>{ + this.indexSelected = 1; + }) + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } + .margin({ bottom:12 }) + + + if(0 == this.indexSelected){ + Scroll(){ + Row(){ + ForEach(this.hairstyleFemales, (hairstyle: TrimStyle, index: number) => { + Stack({ alignContent: Alignment.Bottom }) { + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, // 渐变方向:180度表示从上到下 + colors: [ + ['#FFF4F4F4', 0.0], // 起始颜色:灰色,位置 0% + ['#00FFFFFF', 1.0] // 结束颜色:白色透明,位置 100% + ] + }) + .borderRadius(3.71) + if(hairstyle.name == '无'){ + Stack(){ + Image(hairstyle.icon) + .width(25) + .height(25) + } + .width('100%') + .height('100%') + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.femaleSelected = index; + this.onHairstyleChange?.(hairstyle) + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + }else{ + Image(hairstyle.icon) + .layoutWeight(1) + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.femaleSelected = index; + this.onHairstyleChange?.(hairstyle) + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + } + Text(hairstyle.name) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor('#FFFFFFFF') + .width('100%') + .height(16) + .backgroundColor('#4D000000') + .backdropBlur(9.29) + .borderRadius({bottomLeft:3.71, bottomRight:3.71}) + } + .width(65) + .height(82) + .aspectRatio(65/82) + .borderRadius(3.71) + .borderColor(this.femaleSelected == index? '#FFC2FF43' : '#00000000') + .borderWidth(1.86) + .margin({ left: index==0? 16 : 8, right: index==this.hairstyleFemales.length-1? 16 : 0}) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + }else{ + Scroll(){ + Row(){ + ForEach(this.hairstyleMans, (hairstyle: TrimStyle, index: number) => { + Stack({ alignContent: Alignment.Bottom }) { + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, // 渐变方向:180度表示从上到下 + colors: [ + ['#FFF4F4F4', 0.0], // 起始颜色:灰色,位置 0% + ['#00FFFFFF', 1.0] // 结束颜色:白色透明,位置 100% + ] + }) + .borderRadius(3.71) + if(hairstyle.name == '无'){ + Stack(){ + Image(hairstyle.icon) + .width(25) + .height(25) + } + .width('100%') + .height('100%') + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.manSelected = index; + this.onHairstyleChange?.(hairstyle) + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + }else{ + Image(hairstyle.icon) + .layoutWeight(1) + .onClick(()=>{ + if(cuteViewModel.getProcessedPixelMap()){ + this.manSelected = index; + this.onHairstyleChange?.(hairstyle) + }else{ + ToastUtils.showToast(this.getUIContext(), '请先选择头像'); + } + }) + } + + Text(hairstyle.name) + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor('#FFFFFFFF') + .width('100%') + .height(16) + .backgroundColor('#4D000000') + .backdropBlur(9.29) + .borderRadius({bottomLeft:3.71, bottomRight:3.71}) + } + .width(65) + .height(82) + .aspectRatio(65/82) + .borderRadius(3.71) + .borderColor(this.manSelected == index? '#FFC2FF43' : '#00000000') + .borderWidth(1.86) + .margin({ left: index==0? 16 : 8, right: index==this.hairstyleMans.length-1? 16 : 0}) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + } + } + } +} + +@Component +struct ResizeBar { + onResizeChange?: (resizeStyle: ResizeStyle) => void; + @State indexSelected: number = 0; + @State private resizeStyle: ResizeStyle[] = [ + { id: '1', name: '标准一寸', width:25,height:35 }, + { id: '2', name: '小一寸', width:22,height:32 }, + { id: '3', name: '标准二寸', width:35,height:53 }, + { id: '4', name: '小二寸', width:33,height:48 }, + { id: '5', name: '身份证', width:26,height:32 }, + { id: '6', name: '护照', width:33,height:48 }, + { id: '7', name: '驾驶证', width:22,height:32 }, + { id: '8', name: '社保卡', width:26,height:32 }, + { id: '9', name: '四六级', width:26,height:32 }, + { id: '10', name: '司法考试', width:33,height:48 }, + { id: '11', name: '结婚证', width:40,height:60 }, + { id: '12', name: '中国签证', width:35,height:45 } + ]; + build() { + Column() { + Column() { + Text('选择尺寸') + .fontSize(16) + .fontColor('#FF1A1A1A') + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + .margin({ bottom:14 }) + + Scroll(){ + Row(){ + ForEach(this.resizeStyle, (resizeStyle: ResizeStyle, index: number) => { + Stack({ alignContent: Alignment.Bottom }) { + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, // 渐变方向:180度表示从上到下 + colors: [ + ['#FFF4F4F4', 0.0], // 起始颜色:灰色,位置 0% + ['#00FFFFFF', 1.0] // 结束颜色:白色透明,位置 100% + ] + }) + .borderRadius(3.71) + .borderColor('#FFEDEDED') + .borderWidth(1) + Stack(){ + Column(){ + Text(resizeStyle.name) + .textAlign(TextAlign.Center) + .fontSize(12) + .fontColor('#FF1A1A1A') + .width('100%') + .height(16) + + Text(resizeStyle.width + 'x' + resizeStyle.height + 'mm') + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor('#FFAAAAAA') + .width('100%') + .height(16) + } + } + .width('100%') + .height('100%') + + } + .width(70) + .height(60) + .aspectRatio(70/60) + .borderRadius(3.71) + .borderColor(this.indexSelected == index? '#FFC2FF43' : '#00000000') + .borderWidth(1.86) + .margin({ left: index==0? 16 : 8, right: index == this.resizeStyle.length-1? 16 : 0}) + .onClick(()=>{ + this.indexSelected = index; + + this.onResizeChange?.(resizeStyle) + }) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + } + + } +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/pages/DeleteAccountLayout.ets b/features/mainLayout/src/main/ets/pages/DeleteAccountLayout.ets new file mode 100644 index 0000000..d160207 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/DeleteAccountLayout.ets @@ -0,0 +1,289 @@ +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { ToastDeleteUtils } from '@ohos/common/src/main/ets/dialog/ToastDeleteUtils'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { MainService } from '../viewmodel/MainService'; +import { routerLogin } from '@ohos/common/src/main/ets/router/RouterManager'; + +@Builder +export function DeleteAccountBuilder() { + DeleteAccountLayout() +} + +/** + * 注销页面 + */ +@Component +export struct DeleteAccountLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + + @State isRightsChecked: boolean = false; + onRightsChange: (checked: boolean) => void = () => {}; + + @State isDisputeChecked: boolean = false; + onDisputeChange: (checked: boolean) => void = () => {}; + + @State isMyselfChecked: boolean = false; + onMyselfChange: (checked: boolean) => void = () => {}; + + @State isRecoverChecked: boolean = false; + onRecoverChange: (checked: boolean) => void = () => {}; + + @State textCopyValue: string = ''; + @State textOriginalValue: string = '我自愿注销本账号'; + + build() { + NavDestination() { + Stack() { + Column() { + //标题栏 + TitleBarComponent({titleName:'注销账号', isShowTitle:true, isShowBack:true}) + + Scroll(){ + Column(){ + Image($r('app.media.ic_warning')) + .width('56vp') + .height('56vp') + .margin({ top: 28 }) + .align(Alignment.Center) + + Text('为保证你的账号安全,在你提交的注销申请生效前,需要同时满足以下条件:') + .fontSize(16) + .fontColor('#FF3D3D3D') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .margin({ top: 16 }) + + Column() { + //Item 1 + Row() { + Text('1.放弃当前的VIP权益') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image(this.isRightsChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(20) + .height(20) + .margin({ left: 8}) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isRightsChecked = !this.isRightsChecked; + this.onRightsChange(this.isRightsChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + + } + .width('100%') + .padding({ top:10 , left: 14, right: 14}) + + Text('当前充值的VIP我们不会退款,请解除你的自动续费(如果有),同意请打勾') + .fontSize(13) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .fontWeight(1) + .align(Alignment.Start) + .margin({ top:10 , left: 16, right: 38}) + + //Item 2 + Row() { + Text('2.当前账号没有违规及其他的法律纠纷') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image(this.isDisputeChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(20) + .height(20) + .margin({ left: 8}) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isDisputeChecked = !this.isDisputeChecked; + this.onDisputeChange(this.isDisputeChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + } + .width('100%') + .padding({ top:10 , left: 14, right: 14}) + + Text('同意请打勾') + .fontSize(13) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .fontWeight(1) + .margin({ top:10 , left: 16, right: 38}) + + //Item 3 + Row() { + Text('3.确认是账号本人进行注销操作,自愿承担后果') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image(this.isMyselfChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(20) + .height(20) + .margin({ left: 8}) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isMyselfChecked = !this.isMyselfChecked; + this.onMyselfChange(this.isMyselfChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + } + .width('100%') + .padding({ top:10 , left: 14, right: 14}) + + Text('同意请打勾') + .fontSize(13) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .fontWeight(1) + .margin({ top:10 , left: 16, right: 38}) + + //Item 4 + Row() { + Text('4.账号注销后账号不可登录,不可恢复') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image(this.isRecoverChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(20) + .height(20) + .margin({ left: 8}) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isRecoverChecked = !this.isRecoverChecked; + this.onRecoverChange(this.isRecoverChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + } + .width('100%') + .padding({ top:10 , left: 14, right: 14}) + + Text('同意请打勾') + .fontSize(13) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .fontWeight(1) + .margin({ top:10 , left: 16, right: 38}) + + //Item 5 + Row() { + Text('5.请在输入框中输入以下文字') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + } + .width('100%') + .padding({ top:10 , left: 14, right: 14}) + + Text('我自愿注销本账号') + .fontSize(13) + .fontColor('#FFF64545') + .textAlign(TextAlign.Start) + .fontWeight(1) + .margin({ top:10 , left: 16, right: 38}) + + Stack({ alignContent: Alignment.BottomEnd }) { + TextInput({ placeholder: '', text: this.textCopyValue }) + .width('100%') + .backgroundColor('#FFF6F6F6') + .borderRadius(12) + .maxLength(20) + .borderColor('#FFDEDEDE') + .borderWidth(0.5) + .onChange((v) => this.textCopyValue = v) + } + .padding({ left: 16, right: 16, top: 12, bottom: 12}) + } + .alignItems(HorizontalAlign.Start) + .backgroundColor(Color.White) + .width('100%') + .borderRadius(8) + .margin({ top: 30, bottom: 50}) + .padding({ top: 6, bottom: 6}) + + Button('确认注销') + .onClick(() => { + (this.isRightsChecked && this.isDisputeChecked && this.isMyselfChecked && this.isRecoverChecked && this.textCopyValue == this.textOriginalValue)? + // 确认注销 + MainService.deleteAccount({ + onSuccess: (_) => { + ToastDeleteUtils.showDeleteToast(this.getUIContext(), 5000) + routerLogin('deleteAccount', true) + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), '注销失败!'); + }, + onError: () => { + ToastUtils.showToast(this.getUIContext(), '出错啦~'); + } + }) + : + ToastUtils.showToast(this.getUIContext(), '请确认注销信息') + + }) + .backgroundColor( + (this.isRightsChecked && this.isDisputeChecked && this.isMyselfChecked && this.isRecoverChecked && this.textCopyValue == this.textOriginalValue) + ? '#FFFF4F4F' // 红色 (激活状态) + : '#4dff4f4f' // 灰色 (禁用状态) + ) + .fontColor(Color.White) + .height(44) + .width('90%') + } + .padding({ left: 16, right: 16}) + } + .scrollBar(BarState.Off) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#FFF9F9F9') + } + .hideTitleBar(true) + } +} + + diff --git a/features/mainLayout/src/main/ets/pages/FeedbackLayout.ets b/features/mainLayout/src/main/ets/pages/FeedbackLayout.ets new file mode 100644 index 0000000..4160eda --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/FeedbackLayout.ets @@ -0,0 +1,355 @@ +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { ImageUtils } from '@ohos/common/src/main/ets/utils/ImageUtils'; +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { ImageInfo, UploadInfo } from '../viewmodel/DataBean'; +import { MainService } from '../viewmodel/MainService'; + +@Builder +export function FeedbackBuilder() { + FeedbackLayout() +} + +/** + * 意见反馈页面 + */ +@Component +export struct FeedbackLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + // 数据源 + private options: string[] = ['功能问题', '优化建议', '其他']; + // item1 意见类型单选框索引 + @State selectedIndex: number = 0; + // item2 补充意见输入框文字 + @State textCommentsValue: string = ''; + // item3 意见图片 + @State selectedImages: string[] = []; // 存储选中的图片路径 + private maxCount: number = 3; // 最大选择数量 + // item4 联系方式输入框文字 + @State textContactValue: string = ''; + // 文件上传结果 + @State uploadResult: UploadInfo[] = []; + @State uploadStatus: Record = {}; + + // 调用系统相册选择器 + async selectImage() { + try { + let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); + PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; + PhotoSelectOptions.maxSelectNumber = this.maxCount - this.selectedImages.length; + + let photoPicker = new photoAccessHelper.PhotoViewPicker(); + let photoSelectResult = await photoPicker.select(PhotoSelectOptions); + + if (photoSelectResult.photoUris.length > 0) { + this.selectedImages = this.selectedImages.concat(photoSelectResult.photoUris); + // 开始一张张上传 + for (const uri of photoSelectResult.photoUris) { + try { + this.uploadStatus[uri] = true; + const imageBase64 = await ImageUtils.base64Image(uri); + await MainService.uploadImageBase64(imageBase64, { + onSuccess: (data) => { + let successInfo: UploadInfo = { + isSuccess: true, + message: '上传成功', + data: data as ImageInfo + }; + this.uploadResult.push(successInfo); + this.uploadStatus[uri] = false; + }, + onFailure: (message) => { + let failureInfo: UploadInfo = { + isSuccess: false, + message: message, + data: undefined + }; + this.uploadResult.push(failureInfo); + this.uploadStatus[uri] = false; + + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (err) => { + let errorInfo: UploadInfo = { + isSuccess: false, + message: err.message, + data: undefined + }; + this.uploadResult.push(errorInfo); + this.uploadStatus[uri] = false; + + ToastUtils.showToast(this.getUIContext(), '上传失败'); + } + }); + } catch (e) { + // 如果某一张报错,会跳到这里。 + // 如果你想“某张失败后停止后续上传”,就在这里 break; + // 如果想“不管失败继续下一张”,就什么都不写。 + console.error("顺序上传中断或出错"); + this.uploadStatus[uri] = false; + } + } + console.error("所有图片上传完成"); + } + } catch (err) { + let error: BusinessError = err as BusinessError; + console.error('PhotoViewPicker.select failed: ' + JSON.stringify(error)); + } + } + + build() { + NavDestination() { + Stack() { + Scroll(){ + Column() { + //标题栏 + TitleBarComponent({titleName:'问题反馈', isShowTitle:true, isShowBack:true}) + + // 内容区 + Column() { + // item1 标题 + Row(){ + Text('*') + .fontSize(16) + .fontColor('#FFEA0000') + Text('您想反馈的功能类型') + .fontSize(16) + .textAlign(TextAlign.Start) + .layoutWeight(1) + .fontColor('#FF1A1A1A') + } + .padding({ left: 16, right: 16 }) + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + ForEach(this.options, (item: string, index: number) => { + Text(item) + .fontSize(14) + .fontColor(this.selectedIndex === index ? '#FFC2FF43' : '#FF1A1A1A') + .textAlign(TextAlign.Center) + .layoutWeight(1) // 平分宽度 + .padding({ top: 10, bottom: 10 }) + .margin({ left: 5, right: 5 }) + // 选中状态与非选中状态的背景切换 + .backgroundColor(this.selectedIndex === index ? '#FF252525' : '#FFFFFF') + .borderWidth(1) + .borderColor(this.selectedIndex === index ? Color.Transparent : '#FFD8D8D8') + .borderRadius(8) + // 点击切换索引 + .onClick(() => { + this.selectedIndex = index; + }) + }, (item: string) => item) + } + .width('100%') + .padding(16) + + // item2 标题 + Row(){ + Text('*') + .fontSize(16) + .fontColor('#FFEA0000') + Text('请补充详细问题或意见') + .fontSize(16) + .textAlign(TextAlign.Start) + .layoutWeight(1) + .fontColor('#FF1A1A1A') + } + .padding({ left: 16, right: 16 }) + + Stack({ alignContent: Alignment.BottomEnd }) { + TextArea({ placeholder: '请在这里输入内容' }) + .height(150) + .width('100%') + .backgroundColor('#FFF2F2F2') + .borderRadius(12) + .padding({ bottom: 30 }) // 留出底部空间给字数统计 + .maxLength(200) + .onChange((v) => this.textCommentsValue = v) + + // 手动写字数统计 + Text(`${this.textCommentsValue.length}/200`) + .fontSize(12) + .fontColor('#FFAAAAAA') + .margin({ right: 12, bottom: 12 }) + } + .padding(16) + + // item3 标题(选填) + Row(){ + Text('请补充详细问题或意见') + .fontSize(16) + .textAlign(TextAlign.Start) + .fontColor('#FF1A1A1A') + + Text('(选填,最多可上传三张)') + .fontSize(11) + .textAlign(TextAlign.Start) + .layoutWeight(1) + .fontColor('#FFAAAAAA') + } + .padding({ left: 16, right: 16 }) + + Row({ space: 10 }) { + // 1. 循环显示已选中的图片 + ForEach(this.selectedImages, (uri: string, index: number) => { + /* + Stack(){ + Stack({ alignContent: Alignment.TopEnd }) { + Image(uri) + .width(80) + .height(80) + .borderRadius(8) + .objectFit(ImageFit.Cover) + + // 删除按钮 + Circle({ width: 20, height: 20 }) + .fill('#80000000') + .onClick(() => this.selectedImages.splice(index, 1)) + Text('×').fontColor(Color.White).fontSize(14).margin({ right: 5 }) + } + } + */ + + Stack({ alignContent: Alignment.Center }) { + // 1. 底层:图片 + Image(uri) + .width(80) + .height(80) + .borderRadius(8) + .objectFit(ImageFit.Cover) + + // 2. 中层:右上角删除按钮 (Stack 嵌套实现) + Stack({ alignContent: Alignment.TopEnd }) { + // 占位透明块,撑开点击区域 + Rect().width(80).height(80).fill(Color.Transparent) + + // 实际按钮内容 + Stack({ alignContent: Alignment.Center }) { + Circle({ width: 20, height: 20 }).fill('#80000000') + Text('×').fontColor(Color.White).fontSize(14).textAlign(TextAlign.Center) + } + .offset({ x: 5, y: -5 }) // 往右上角偏移一点,视觉效果更好 + .onClick(() => { + MainService.deleteImage((this.uploadResult[index].data?.id || ''), { + onSuccess: (_) => { + this.selectedImages.splice(index, 1); + this.uploadResult.splice(index, 1); + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: () => { + ToastUtils.showToast(this.getUIContext(), '删除失败'); + } + }); + }) + } + .width(80) + .height(80) + + // 3. 顶层:圆形加载效果 (仅在加载时显示) + if (this.uploadStatus[this.selectedImages[index]] === true) { + LoadingProgress() + .width(30) + .height(30) + .color('#CCCCCC') // 设置旋转圆圈的颜色 + } + } + }) + + // 2. 添加按钮(当数量小于 3 时显示) + if (this.selectedImages.length < this.maxCount) { + Column() { + Image($r('app.media.ic_image_add')) // 替换为你图片中的虚线加号图标 + .width(24) + .height(24) + } + .width(80) + .height(80) + .backgroundColor('#FFF2F2F2') + .borderRadius(8) + .justifyContent(FlexAlign.Center) + .onClick(() => this.selectImage()) + } + } + .width('100%') + .padding(16) + + // item4 标题(选填) + Row(){ + Text('联系方式') + .fontSize(16) + .textAlign(TextAlign.Start) + .fontColor('#FF1A1A1A') + + Text('(选填,可以留下您的手机号、微信、邮箱)') + .fontSize(11) + .textAlign(TextAlign.Start) + .layoutWeight(1) + .fontColor('#FFAAAAAA') + } + .padding({ left: 16, right: 16 }) + + Stack({ alignContent: Alignment.BottomEnd }) { + TextInput({ placeholder: '请输入您的联系方式', text: this.textContactValue }) + .width('100%') + .backgroundColor('#FFF2F2F2') + .borderRadius(12) + .maxLength(20) + .type(InputType.PhoneNumber) + .onChange((v) => this.textContactValue = v) + } + .padding(16) + + } + + } + .width('100%') + .height('100%') + } + .layoutWeight(1) + .scrollBar(BarState.Off) + //提交按钮 + Stack() { + Button('提交') + .width('100%') + .height(48) + .fontSize(16) + .fontColor('#FFC2FF43') + .backgroundColor('#FF252525') + .borderRadius(359) + .onClick(() => { + // 提交逻辑 + if(!this.textCommentsValue){ + ToastUtils.showToast(this.getUIContext(), '请填写反馈内容'); + return + } + const type = this.options[this.selectedIndex] + const images: string[] = [] + for (let i = 0; i < this.uploadResult.length; i++) { + images.push(this.uploadResult[i].data?.url || '') + } + MainService.feedback(type, this.textCommentsValue, this.textContactValue, images,{ + onSuccess: (_) => { + ToastUtils.showToast(this.getUIContext(), '感谢您的反馈'); + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), '对不起,提交失败'); + }, + onError: () => { + ToastUtils.showToast(this.getUIContext(), '出错啦~'); + } + }); + }) + } + .margin({ left: 16, right: 16, bottom: 32 }) + } + .width('100%') + .height('100%') + .alignContent(Alignment.Bottom) + } + .hideTitleBar(true) + } + + +} diff --git a/features/mainLayout/src/main/ets/pages/FormatLayout.ets b/features/mainLayout/src/main/ets/pages/FormatLayout.ets new file mode 100644 index 0000000..67a7e59 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/FormatLayout.ets @@ -0,0 +1,210 @@ +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { FormatStyle } from '../viewmodel/LocalBean'; +import { image } from '@kit.ImageKit'; +import { ImageUtils } from '@ohos/common/src/main/ets/utils/ImageUtils'; +import { promptAction } from '@kit.ArkUI'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; + +@Builder +export function FormatBuilder() { + FormatLayout() +} + +/** + * 格式转换页面 + */ +@Component +export struct FormatLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + @State processedPixelMap: image.PixelMap | null = null; + @State formatStyle: FormatStyle | null = null; + + aboutToAppear(): void { + ImageUtils.openGallery().then(item => { + if (item) { + this.processedPixelMap = item.pixelMap; + } + }) + } + + build() { + NavDestination() { + Column() { + //标题栏 + TitleBarComponent({ + titleName: '格式转换', + isShowTitle: false, + isShowBack: true, + isShowSave: true, + onSave: () => { + if(this.processedPixelMap){ + if(this.formatStyle?.type === 0 ){ + //jpeg + ImageUtils.saveFormatToGallery(this.processedPixelMap, 'image/jpeg') + }else if(this.formatStyle?.type === 1 ){ + //png + ImageUtils.saveFormatToGallery(this.processedPixelMap, 'image/png') + }else if(this.formatStyle?.type === 2 ){ + //gif + ImageUtils.saveFormatToGallery(this.processedPixelMap, 'image/gif') + }else if(this.formatStyle?.type === 3 ){ + ImageUtils.saveSvgToGallery(this.processedPixelMap) + } + }else{ + ToastUtils.normalToast({ + message: '请选择图片', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + } + } + }) + + Stack(){ + Stack({alignContent:Alignment.Top}){ + if(this.processedPixelMap == null){ + Column(){ + Stack() { + Column(){ + Image($r('app.media.ic_image_pld')).width(120).height(111.2) + Text('去相册选一张美照吧~').fontColor('#FFD8D8D8').fontSize(12) + } + } + .width('80%') + .aspectRatio(300/420) + .backgroundColor('#FFFFFFFF') + .borderColor('#FFD8D8D8') + .borderWidth(1) + .onClick(() => { + ImageUtils.openGallery().then(item => { + if (item) { + this.processedPixelMap = item.pixelMap; + } + }) + }) + + Text('请选择图片导出格式,用于适配各种平台') + .fontSize(14) + .fontColor('#FFFFFFFF') + .width('90%') + .borderRadius(7) + .backgroundColor('#FFAAAAAA') + .margin({ top: 30 }) + .padding({ left: 14, right: 14, top: 8, bottom: 8}) + } + }else{ + Column() { + Stack() { + Stack() { + Image(this.processedPixelMap) + .width('100%') + .height('100%') + .objectFit(ImageFit.Contain) + } + .backgroundColor('#FFFFFFFF') + .id('resize_container') + .width('80%') + .aspectRatio(300/420) + .clip(true) + } + .backgroundColor('#FFFFFFFF') + .borderWidth(1) + .borderColor('#FFD8D8D8') + + Text('请选择图片导出格式,用于适配各种平台') + .fontSize(14) + .fontColor('#FFFFFFFF') + .width('90%') + .borderRadius(7) + .backgroundColor('#FFAAAAAA') + .margin({ top: 30 }) + .padding({ left: 14, right: 14, top: 8, bottom: 8}) + } + } + } + .width('100%') + .height('100%') + + FormatBar({ + onFormatChange: (formatStyle: FormatStyle) => { + this.formatStyle = formatStyle; + } + }) + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .width('100%') + .height('100%') + .backgroundColor('#FFF4F4F4') + } + .hideTitleBar(true) + } +} + + +@Component +struct FormatBar { + onFormatChange?: (formatStyle: FormatStyle) => void; + @State indexSelected: number = 0; + @State private formatStyle: FormatStyle[] = [ + { id: '1', name: 'JPG', type: 0 }, + { id: '2', name: 'PNG', type: 1 }, + { id: '3', name: 'GIF', type: 2 }, + { id: '4', name: 'SVG', type: 3 } + ]; + build() { + Column() { + Column() { + Text('格式转换') + .fontSize(16) + .fontColor('#FF1A1A1A') + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + .margin({ bottom:14 }) + + Stack().width('100%').height(0.5) + .margin({ bottom:12 }) + .backgroundColor('#FFEDEDED') + + Scroll(){ + Row(){ + ForEach(this.formatStyle, (formatStyle: FormatStyle, index: number) => { + Stack({ alignContent: Alignment.Bottom }) { + Text(formatStyle.name) + .textAlign(TextAlign.Center) + .fontSize(12) + .fontColor('#FF1A1A1A') + .width('100%') + .height(16) + } + .alignContent(Alignment.Center) + .width(70) + .height(34) + .aspectRatio(70/34) + .borderRadius(242) + .borderColor(this.indexSelected == index? '#FF000000' : '#FFEDEDED') + .borderWidth(1) + .margin({ left: index==0? 16 : 8, right: index == this.formatStyle.length-1? 16 : 0}) + .onClick(()=>{ + this.indexSelected = index; + + this.onFormatChange?.(formatStyle) + }) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + } + .backgroundColor('#FFFFFFFF') + .borderRadius({ topLeft: 16, topRight: 16}) + .padding({top: 14, bottom: 42}) + + } +} diff --git a/features/mainLayout/src/main/ets/pages/GuideLayout.ets b/features/mainLayout/src/main/ets/pages/GuideLayout.ets new file mode 100644 index 0000000..303e785 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/GuideLayout.ets @@ -0,0 +1,184 @@ +import { TitleBarComponent } from '../view/TitleBarComponent'; + +@Builder +export function GuideBuilder() { + GuideLayout() +} + +/** + * 拍照指南页面 + */ +@Component +export struct GuideLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + + build() { + NavDestination() { + Stack(){ + Stack() { + Image($r('app.media.ic_guide_watermark')) + .width(185) + .height(185) + } + .width('100%') + .alignContent(Alignment.End) + Column() { + //标题栏 + TitleBarComponent({ + titleName: '', + isShowTitle: false, + isShowBack: true, + isShowSave: false, + onSave: () => { + } + }) + + Scroll(){ + // 内容区 + Column() { + Text('如何拍照?') + .width('100%') + .fontSize(24) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Bold) + + Stack(){ + Column(){ + Text('1.注意光线均匀\n' + + '2.不能耸肩或斜肩\n' + + '3.正对镜头,双耳露出\n' + + '4.不要佩戴眼镜\n' + + '5.注意纯色墙作背景\n' + + '6.避免衣服与背景色相同') + .width('100%') + .lineHeight(27) + .fontSize(14) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + } + .justifyContent(FlexAlign.Start) + .width('100%') + .padding({ left: 16, right: 16, top: 20, bottom: 20 }) + + Column(){ + Image($r('app.media.ic_guide_example')) + .width(93) + .height(130) + + Text('照片示例') + .fontSize(12) + .fontColor('#FFFFFFFF') + .fontWeight(FontWeight.Normal) + .backgroundColor('#801a1a1a') + .borderRadius(4) + .padding({left: 8, right: 8, top: 5, bottom: 5}) + .margin({top: 12}) + } + .margin({right: 16, top:11}) + .align(Alignment.Top) + } + .alignContent(Alignment.TopEnd) + .width('100%') + .borderRadius(16) + .backgroundColor('#FFFFFFFF') + .padding({bottom: 10}) + .margin({left: 16, right: 16, top: 20}) + + + Text('证件照背景颜色要求') + .width('100%') + .fontSize(24) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Bold) + .margin({top: 24}) + + Stack(){ + Column(){ + // 白色背景 + Row(){ + Stack().backgroundColor('#FFFFFFFF').borderRadius(369).width(18).height(18).borderColor('#FFAAAAAA').borderWidth(0.5) + Text('白色背景:') + .width('100%') + .fontSize(12) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + .margin({left: 4}) + } + Text('用于护照、签证、驾驶证、身份证、二代身份证、驾驶证、黑白证件、医保卡、港澳通行证、护照等。') + .width('100%') + .fontSize(12) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + .margin({top: 12}) + + // 蓝色背景 + Row(){ + Stack().backgroundColor('#FF438EDB').borderRadius(369).width(18).height(18).borderColor('#FF438EDB').borderWidth(0.5) + Text('蓝色背景:') + .width('100%') + .fontSize(12) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + .margin({left: 4}) + } + .margin({top: 24}) + Text('用于毕业证、工作证、简历等\n' + + '(蓝色数值为R:0 G:191 B:243 或 C:67 M:Z Y:0 k:0)') + .width('100%') + .fontSize(12) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + .margin({top: 12}) + + // 红色背景 + Row(){ + Stack().backgroundColor('#FFFF0000').borderRadius(369).width(18).height(18).borderColor('#FFFF0000').borderWidth(0.5) + Text('红色背景:') + .width('100%') + .fontSize(12) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + .margin({left: 4}) + } + .margin({top: 24}) + Text('用于保险、医保、IC卡、暂住证、结婚照\n' + + '(红色数值为R:255 G:0 B:0 或 C:0 M:99 Y:100 K: 0)') + .width('100%') + .fontSize(12) + .fontColor('#FF1A1A1A') + .fontWeight(FontWeight.Normal) + .margin({top: 12}) + } + .justifyContent(FlexAlign.Start) + .width('100%') + .padding({ left: 16, right: 16, top: 20, bottom: 20 }) + } + .alignContent(Alignment.TopEnd) + .width('100%') + .borderRadius(16) + .backgroundColor('#FFFFFFFF') + .margin({left: 16, right: 16, top: 20}) + } + .margin({top: 16 }) + .width('100%') + .padding({left: 16, right: 16}) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Off) + .align(Alignment.TopStart) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#FFF4F4F4') + .alignContent(Alignment.TopStart) + } + .hideTitleBar(true) + } + + +} diff --git a/features/mainLayout/src/main/ets/pages/LongImageLayout.ets b/features/mainLayout/src/main/ets/pages/LongImageLayout.ets new file mode 100644 index 0000000..10ece8f --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/LongImageLayout.ets @@ -0,0 +1,191 @@ +import { ImageUtils } from '@ohos/common/src/main/ets/utils/ImageUtils'; +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { image } from '@kit.ImageKit'; +import { ImageCropper, ImageCropperController } from '@candies/image_cropper'; +import * as image_cropper from "@candies/image_cropper"; + +@Builder +export function LongImageBuilder() { + LongImageLayout() +} + +/** + * 拼长图页面 + */ +@Component +export struct LongImageLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + @State images: image.PixelMap[] = []; + + @State editingIndex: number = -1; + private cropperController: ImageCropperController = new ImageCropperController(); + @State config: image_cropper.ImageCropperConfig = new image_cropper.ImageCropperConfig( + { + maxScale: 8, + cropRectPadding: image_cropper.geometry.EdgeInsets.all(20), + controller: this.cropperController, + initCropRectType: image_cropper.InitCropRectType.imageRect, + cropAspectRatio: image_cropper.CropAspectRatios.custom, + } + ); + @State imageSource: image.ImageSource | undefined = undefined; + // 生成图片完成标志 + @State generateImageFlag: boolean = false; + + + @Builder pixelMapBuilder(pm: image.PixelMap) { + Image(pm) + .width('90%') + .objectFit(ImageFit.Cover) + .opacity(0.8) + } + + aboutToAppear(): void { + this.config.controller = this.cropperController; + ImageUtils.openMultipleGallery().then(item => { + if (item) { + this.images = item; + } + }) + } + + build() { + NavDestination() { + Stack() { + Column() { + // 标题栏 + TitleBarComponent({ + titleName: '拼长图', + isShowTitle: true, + isShowBack: true, + isShowSave: true, + onSave: () => { + /* 保存整张长图逻辑 */ + this.generateImageFlag = true; + setTimeout(async () => { + try { + await ImageUtils.saveImageForID('crop_container'); + } catch (e) { + console.error("保存失败: " + e); + } finally { + this.generateImageFlag = false; + } + }, 100); + } + }) + + Scroll() { + Column() { + if (this.images.length > 0){ + //有数据展示列表 + Grid() { + ForEach(this.images, (pm: image.PixelMap, index: number) => { + GridItem() { + Stack({ alignContent: Alignment.TopEnd }) { + Image(pm) + .width('100%') + .objectFit(ImageFit.Cover) + .parallelGesture( + TapGesture().onAction(async () => { + this.editingIndex = index; + const source = await ImageUtils.pixelMapToImageSource(pm); + this.imageSource = source; + }) + ) + + // 删除按钮 + Image($r('app.media.ic_close')) // 建议用图片图标 + .width(24).height(24) + .margin({ top: 8, right: 8 }) + .onClick(() => this.images.splice(index, 1)) + .visibility(this.generateImageFlag ? Visibility.None : Visibility.Visible) + } + } + }, (pm: image.PixelMap, index: number) => { + return index + "_" + pm.getPixelBytesNumber(); + }) + } + .columnsTemplate('1fr') + .padding(16) + .editMode(true) + .id('crop_container') + }else{ + //无数据展示占位 + Stack() { + Column(){ + Image($r('app.media.ic_image_pld')).width(120).height(111.2) + Text('去相册选一张美照吧~').fontColor('#FFD8D8D8').fontSize(12) + } + } + .width('80%') + .height('90%') + // .aspectRatio(300/420) + .backgroundColor('#FFFFFFFF') + .borderColor('#FFD8D8D8') + .borderWidth(1) + .onClick(() => { + ImageUtils.openMultipleGallery().then(item => { + if (item) { + this.images = item; + } + }) + }) + } + } + } + .layoutWeight(1) + .scrollBar(BarState.Off) + .align(Alignment.Top) + } + .width('100%') + .height('100%') + + // 裁剪工具层 + if (this.imageSource) { + Column(){//({ alignContent: Alignment.Bottom }) { + ImageCropper({ + image: this.imageSource, + config: this.config + }) + .width('100%') + .height('100%') + .layoutWeight(1) + .backgroundColor(Color.Black) + + Row() { + Text('取消') + .fontColor('#ffb3b1b1') + .fontSize(16) + .onClick(() => { + this.imageSource = undefined; + this.editingIndex = -1; + }) + Text('确定') + .fontColor(Color.White) + .fontSize(16) + .onClick(async () => { + const croppedPm = await this.cropperController.getCroppedImage(); + if (croppedPm && this.editingIndex !== -1) { + this.images.splice(this.editingIndex, 1, croppedPm); + } + this.imageSource = undefined; + this.editingIndex = -1; + }) + } + .width('100%') + .backgroundColor('#ff8f8d8d') + .padding({ left: 20, right: 20, top:12, bottom: 30 }) + .justifyContent(FlexAlign.SpaceBetween) + } + .width('100%') + .height('100%') + .zIndex(100) + } + } + } + .hideTitleBar(true) + } +} + + diff --git a/features/mainLayout/src/main/ets/pages/ResizeLayout.ets b/features/mainLayout/src/main/ets/pages/ResizeLayout.ets new file mode 100644 index 0000000..5ad3228 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/ResizeLayout.ets @@ -0,0 +1,232 @@ +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { CuteParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { ImageUtils } from '@ohos/common/src/main/ets/utils/ImageUtils'; +import { StickerItem } from '@ohos/common/src/main/ets/viewmodel/LocalBean'; +import { ZoomView } from '../view/ZoomView'; +import { ResizeStyle } from '../viewmodel/LocalBean'; + +@Builder +export function ResizeBuilder() { + ResizeLayout() +} + +/** + * 改尺寸页面 + */ +@Component +export struct ResizeLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + @State cuteParams: CuteParams | null = null; + @State stickerItem: StickerItem | null = null; + + aboutToAppear(): void { + ImageUtils.openGallery().then(item => { + if (item) { + this.stickerItem = item; + } + }) + } + + build() { + NavDestination() { + Column() { + //标题栏 + TitleBarComponent({ + titleName: '改尺寸', + isShowTitle: false, + isShowBack: true, + isShowSave: true, + onSave: () => { + ImageUtils.saveImageForID('resize_container') + } + }) + + + Stack(){ + Stack({alignContent:Alignment.Top}){ + if(this.stickerItem == null){ + Column(){ + Stack() { + Column(){ + Image($r('app.media.ic_image_pld')).width(120).height(111.2) + Text('去相册选一张美照吧~').fontColor('#FFD8D8D8').fontSize(12) + } + } + .width('80%') + .aspectRatio((this.cuteParams?.width??300) / (this.cuteParams?.height??420))//300/420 + .backgroundColor('#FFFFFFFF') + .borderColor('#FFD8D8D8') + .borderWidth(1) + .onClick(() => { + ImageUtils.openGallery().then(item => { + if (item) { + this.stickerItem = item; + } + }) + }) + + Text('将已有证件照尺寸进行修改,用于适配各种场景') + .fontSize(14) + .fontColor('#FFFFFFFF') + .width('90%') + .borderRadius(7) + .backgroundColor('#FFAAAAAA') + .margin({ top: 30 }) + .padding({ left: 14, right: 14, top: 8, bottom: 8}) + } + }else{ + Column() { + Stack() { + Stack() { + ZoomView({item: this.stickerItem}) + .width('100%') + .height('100%') + } + .backgroundColor('#FFFFFFFF') + .id('resize_container') + .width('80%') + .aspectRatio((this.cuteParams?.width??300) / (this.cuteParams?.height??420)) + .clip(true) + } + .backgroundColor('#FFFFFFFF') + .borderWidth(1) + .borderColor('#FFD8D8D8') + + Text('将已有证件照尺寸进行修改,用于适配各种场景') + .fontSize(14) + .fontColor('#FFFFFFFF') + .width('90%') + .borderRadius(7) + .backgroundColor('#FFAAAAAA') + .margin({ top: 30 }) + .padding({ left: 14, right: 14, top: 8, bottom: 8}) + } + } + } + .width('100%') + .height('100%') + + ResizeBar({ + onResizeChange: (resizeStyle: ResizeStyle) => { + let name = resizeStyle.name + let width = resizeStyle.width + let height = resizeStyle.height + if (this.cuteParams) { + this.cuteParams.width = width; + this.cuteParams.height = height; + } else { + this.cuteParams = { id: 0, name: name, width: width, height: height }; + } + } + }) + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .width('100%') + .height('100%') + .backgroundColor('#FFF4F4F4') + } + .hideTitleBar(true) + } +} + +@Component +struct ResizeBar { + onResizeChange?: (resizeStyle: ResizeStyle) => void; + @State indexSelected: number = 0; + @State private resizeStyle: ResizeStyle[] = [ + { id: '1', name: '标准一寸', width:25,height:35 }, + { id: '2', name: '小一寸', width:22,height:32 }, + { id: '3', name: '标准二寸', width:35,height:53 }, + { id: '4', name: '小二寸', width:33,height:48 }, + { id: '5', name: '身份证', width:26,height:32 }, + { id: '6', name: '护照', width:33,height:48 }, + { id: '7', name: '驾驶证', width:22,height:32 }, + { id: '8', name: '社保卡', width:26,height:32 }, + { id: '9', name: '四六级', width:26,height:32 }, + { id: '10', name: '司法考试', width:33,height:48 }, + { id: '11', name: '结婚证', width:40,height:60 }, + { id: '12', name: '中国签证', width:35,height:45 } + ]; + build() { + Column() { + Column() { + Text('选择尺寸') + .fontSize(16) + .fontColor('#FF1A1A1A') + Image($r('app.media.ic_title_indicator')) + .width(32) + .height(5) + } + .margin({ bottom:14 }) + + Stack().width('100%').height(0.5) + .margin({ bottom:12 }) + .backgroundColor('#FFEDEDED') + + Scroll(){ + Row(){ + ForEach(this.resizeStyle, (resizeStyle: ResizeStyle, index: number) => { + Stack({ alignContent: Alignment.Bottom }) { + Column() + .width('100%') + .height('100%') + .linearGradient({ + angle: 180, // 渐变方向:180度表示从上到下 + colors: [ + ['#FFF4F4F4', 0.0], // 起始颜色:灰色,位置 0% + ['#00FFFFFF', 1.0] // 结束颜色:白色透明,位置 100% + ] + }) + .borderRadius(3.71) + .borderColor('#FFEDEDED') + .borderWidth(1) + Stack(){ + Column(){ + Text(resizeStyle.name) + .textAlign(TextAlign.Center) + .fontSize(12) + .fontColor('#FF1A1A1A') + .width('100%') + .height(16) + + Text(resizeStyle.width + 'x' + resizeStyle.height + 'mm') + .textAlign(TextAlign.Center) + .fontSize(10) + .fontColor('#FFAAAAAA') + .width('100%') + .height(16) + } + } + .width('100%') + .height('100%') + + } + .width(70) + .height(60) + .aspectRatio(70/60) + .borderRadius(3.71) + .borderColor(this.indexSelected == index? '#FFC2FF43' : '#00000000') + .borderWidth(1.86) + .margin({ left: index==0? 16 : 8, right: index == this.resizeStyle.length-1? 16 : 0}) + .onClick(()=>{ + this.indexSelected = index; + + this.onResizeChange?.(resizeStyle) + }) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + } + .backgroundColor('#FFFFFFFF') + .borderRadius({ topLeft: 16, topRight: 16}) + .padding({top: 14, bottom: 30}) + + } +} diff --git a/features/mainLayout/src/main/ets/pages/SettingLayout.ets b/features/mainLayout/src/main/ets/pages/SettingLayout.ets new file mode 100644 index 0000000..818081b --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/SettingLayout.ets @@ -0,0 +1,321 @@ +import { promptAction } from '@kit.ArkUI'; +import { fileIo, storageStatistics } from '@kit.CoreFileKit'; +import { TipsDialog } from '@ohos/common/src/main/ets/dialog/TipsDialog'; +import { TitleBarComponent } from '../view/TitleBarComponent'; +import { common, contextConstant } from '@kit.AbilityKit'; +import { routerLogin } from '@ohos/common/src/main/ets/router/RouterManager'; +import { USER_PROFILE } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { UserProfile } from '@ohos/common/src/main/ets/viewmodel/DataBean'; +import { emitter } from '@kit.BasicServicesKit'; +import { EVT_USER_INFO } from '@ohos/common/src/main/ets/constants/EventConstants'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { MainService } from '../viewmodel/MainService'; + +@Builder +export function SettingBuilder() { + SettingLayout() +} + +/** + * 设置页面 + */ +@Component +export struct SettingLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + @State appCacheSize: string = ''; + @State isLogin: boolean = false; + + dialogController: CustomDialogController = new CustomDialogController({ + builder: TipsDialog({ + title: '清除缓存', + content: '清除缓存后可能会丢失聊天数据', + cancel: () => { console.info('用户点击取消') }, + confirm: () => { + clearAllCache(getContext(this) as common.UIAbilityContext).then(value => { + if(value){ + getAppCacheSize().then(v => this.appCacheSize = v); + ToastUtils.normalToast({ + message: '清除缓存成功', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + }else{ + ToastUtils.normalToast({ + message: '清除缓存失败', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + } + }) + + } + }), + alignment: DialogAlignment.Center, // 居中显示 + customStyle: true, // 使用自定义样式,去掉系统默认背景 + autoCancel: true // 点击遮罩层可关闭 + }) + + aboutToAppear() { + getAppCacheSize().then(v => this.appCacheSize = v); + this.isLogin = !AppStorage.get(USER_PROFILE)?.temp; + + // 事件监听 + emitter.on({ eventId: EVT_USER_INFO }, (eventData: emitter.EventData) => { + const payload = eventData.data; + if (payload) { + const result = payload["result"] as boolean; + if (result === true) { + //记得一定要延迟更新,否则接口未请求完成 + setTimeout(() => { + this.isLogin = !AppStorage.get(USER_PROFILE)?.temp; + }, 1000); + } + } + }); + } + + build() { + NavDestination() { + Stack() { + Scroll(){ + Column() { + //标题栏 + TitleBarComponent({titleName:'设置', isShowTitle:true, isShowBack:true}) + + // 内容区 + Column(){ + Column() { + Row() { + Text('清除缓存') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + + Row(){ + Text(this.appCacheSize) + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + // 弹出确认窗 + this.dialogController.open() + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Text('账号绑定') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + this.pageStack.pushPathByName("AccountBindLayout", null); + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Text('账号管理') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + this.pageStack.pushPathByName("AccountManagerLayout", null); + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Text('关于我们') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + this.pageStack.pushPathByName("AboutUsLayout", null); + }) + + if(this.isLogin){ + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Text('注销账号') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8, right: 14}) + } + .width('100%') + .height('52vp') + .padding({ left: 14}) + .onClick(() => { + // 注销账号 + this.pageStack.pushPathByName("DeleteAccountLayout", null); + }) + } + + } + .backgroundColor(Color.White) + .width('100%') + .borderRadius(12) + .margin({ top: 30}) + } + .padding({ left: 16, right: 16}) + + } + .width('100%') + .height('100%') + } + .layoutWeight(1) + .scrollBar(BarState.Off) + //底部按钮 + Stack() { + if(this.isLogin){ + Column(){ + Button('账号切换') + .width('100%') + .height(48) + .fontSize(16) + .fontColor('#FF1A1A1A') + .backgroundColor('#FFFFFF') + .borderColor('#FF1A1A1A') + .borderWidth(0.5) + .borderRadius(359) + .margin({ bottom: 16 }) + .onClick(() => { + this.pageStack.pushPathByName("AccountManagerLayout", null); + }) + Button('退出登录') + .width('100%') + .height(48) + .fontSize(16) + .fontColor('#FFC2FF43') + .backgroundColor('#FF252525') + .borderRadius(359) + .onClick(() => { + // 退出登录 + + MainService.logout({ + onSuccess: (_) => { + routerLogin('logout',true) + }, + onFailure:(_) =>{ + ToastUtils.normalToast({ message: '退出失败啦~' }); + } + }) + }) + } + } + } + .margin({ left: 16, right: 16, bottom: 32 }) + } + .width('100%') + .height('100%') + .alignContent(Alignment.Bottom) + .backgroundColor('#FFF9F9F9') + } + .hideTitleBar(true) + } +} + +/** + * 获取当前应用的缓存大小 + * @returns 格式化后的字符串 (如 "12.50 MB") + */ +async function getAppCacheSize(): Promise { + try { + // 获取当前应用的存储状态(包括数据大小、缓存大小等) + let bundleStats = await storageStatistics.getCurrentBundleStats(); + let cacheSizeByte = bundleStats.cacheSize; // 单位是 Byte (字节) + + // 转换为 MB 并保留两位小数 + let cacheSizeMB = (cacheSizeByte / (1024 * 1024)).toFixed(2); + return `${cacheSizeMB} MB`; + } catch (err) { + let error: BusinessError = err as BusinessError; + console.error('获取缓存失败: ' + JSON.stringify(error)); + return '0.00 MB'; + } +} + +/** + * 彻底清理所有区域的缓存目录 + */ +async function clearAllCache(context: common.UIAbilityContext): Promise { + context.getApplicationContext().area = contextConstant.AreaMode.EL1; + context.area = contextConstant.AreaMode.EL1; + const el1AppCacheDir = context.getApplicationContext().cacheDir + const el1HapCacheDir = context.cacheDir + context.getApplicationContext().area = contextConstant.AreaMode.EL2; + context.area = contextConstant.AreaMode.EL2; + const el2AppCacheDir = context.getApplicationContext().cacheDir + const el2HapCacheDir = context.cacheDir + + const clearTask = [ + clearCacheTask(el1AppCacheDir), + clearCacheTask(el1HapCacheDir), + clearCacheTask(el2AppCacheDir), + clearCacheTask(el2HapCacheDir) + ] + + await Promise.all(clearTask) + return true +} +// 清除缓存任务 +function clearCacheTask(dir: string): Promise { + return new Promise((resolve) => { + fileIo.access(dir).then((exist: boolean) => { + if (exist) { + fileIo.rmdir(dir) + } + resolve(true) + }) + }) +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/pages/WebLayout.ets b/features/mainLayout/src/main/ets/pages/WebLayout.ets new file mode 100644 index 0000000..e9d92e4 --- /dev/null +++ b/features/mainLayout/src/main/ets/pages/WebLayout.ets @@ -0,0 +1,51 @@ +import { WebParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { webview } from '@kit.ArkWeb'; +import { TitleBarComponent } from '../view/TitleBarComponent'; + +@Builder +export function WebLayoutBuilder() { + WebLayout() +} + +/** + * 内置Web页面 + */ +@Component +export struct WebLayout { + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + @State urlParams: WebParams | null = null; + + controller: webview.WebviewController = new webview.WebviewController(); + + build() { + NavDestination() { + Stack() { + Column() { + //标题栏 + TitleBarComponent({titleName:this.urlParams?.name, isShowTitle:true, isShowBack:true}) + + Column(){ + Web({ + src: this.urlParams?.url, + controller: this.controller // 绑定控制器 + }) + .onPageEnd(() => { + // 页面加载完成后执行 JS + this.controller.runJavaScript('document.title = "HarmonyOS"'); + }) + } + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + } + .onReady((context: NavDestinationContext) => { + this.urlParams = context.pathInfo.param as WebParams; + }) + .hideTitleBar(true) + } +} + diff --git a/features/mainLayout/src/main/ets/view/GridComponent.ets b/features/mainLayout/src/main/ets/view/GridComponent.ets new file mode 100644 index 0000000..fb9c87b --- /dev/null +++ b/features/mainLayout/src/main/ets/view/GridComponent.ets @@ -0,0 +1,97 @@ +import { BreakPointType } from '@ohos/common'; +import { commonConstants } from '../constants/CommonConstants'; + +/** + * Grid component. + */ +@Component +export struct GridComponent { + @StorageProp('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName; + @State colTemplate: string = commonConstants.initializeTemplate; + private readonly data: number[] = []; + + aboutToAppear() { + for (let i = 0; i < commonConstants.gridSize; i++) { + this.data.push(i); + } + switch (this.currentBreakpoint) { + case commonConstants.breakpointsSmName: + this.colTemplate = commonConstants.smColTemplate; + break; + case commonConstants.breakpointsMdName: + this.colTemplate = commonConstants.mdColTemplate; + break; + case commonConstants.breakpointsLgName: + this.colTemplate = commonConstants.lgColTemplate; + break; + case commonConstants.breakpointsXlName: + this.colTemplate = commonConstants.xlColTemplate; + break; + default: + this.colTemplate = commonConstants.smColTemplate; + break; + } + } + + build() { + Column() { + Row() { + Stack() { + SymbolGlyph($r('sys.symbol.chevron_left')) + .fontSize($r('app.float.arrow_width')) + .onClick(() => { + this.getUIContext().getRouter().back(); + }) + .fontColor([$r('sys.color.titlebar_icon_color')]) + } + .width($r('app.float.arrow_width')) + .borderRadius($r('app.float.arrow_left_borderRadius')) + .margin({ left: $r('app.float.arrow_margin_left') }) + .backgroundColor($r('sys.color.ohos_id_color_button_normal')) + } + .width(commonConstants.rowWidth) + .height($r('app.float.arrow_item_height')) + + Grid() { + ForEach(this.data, () => { + GridItem() { + Column() { + Row() + .width(commonConstants.rowWidth) + .aspectRatio(commonConstants.columnAspectRatio) + .backgroundColor($r('sys.color.ohos_id_color_component_normal')) + .borderRadius($r('app.float.grid_row_border_radius')) + + Row() { + Text($r('app.string.grid_title_label')) + .margin({ top: $r('app.float.grid_text_top') }) + .fontSize($r('app.float.grid_text_font_size')) + .fontWeight(commonConstants.columnTextFontWeight) + .fontColor($r('sys.color.ohos_id_color_text_primary')) + } + .padding({ left: $r('app.float.grid_text_padding_left') }) + .width(commonConstants.rowWidth) + } + } + }, (item: number) => item.toString()) + } + .columnsGap($r('app.float.grid_column_gap')) + .rowsGap($r('app.float.grid_row_gap')) + .columnsTemplate(this.colTemplate) + .onAreaChange(() => { + this.colTemplate = new BreakPointType({ + sm: commonConstants.smColTemplate, + md: commonConstants.mdColTemplate, + lg: commonConstants.lgColTemplate, + xl: commonConstants.xlColTemplate + }).getValue(this.currentBreakpoint); + }) + .margin({ + left: $r('app.float.grid_margin_left'), + right: $r('app.float.grid_margin_right') + }) + .padding({ bottom: $r('app.float.grid_padding_bottom') }) + .scrollBar(BarState.Off) + } + } +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/view/StickerView.ets b/features/mainLayout/src/main/ets/view/StickerView.ets new file mode 100644 index 0000000..07fdc14 --- /dev/null +++ b/features/mainLayout/src/main/ets/view/StickerView.ets @@ -0,0 +1,65 @@ +import { StickerItem } from '@ohos/common/src/main/ets/viewmodel/LocalBean'; + +@Component +export struct StickerView { + @ObjectLink item: StickerItem; + @Link selectedIndex: number; // 改用 @Link 关联父组件的索引,实现实时同步 + @Prop index: number; // 传入当前在 ForEach 中的索引 + + build() { + Image(this.item.pixelMap) + .width(200) + .height(200) + .objectFit(ImageFit.Contain) + .translate({ x: this.item.posX, y: this.item.posY }) + .scale({ x: this.item.scaleValue, y: this.item.scaleValue }) + .rotate({ angle: this.item.rotateValue }) + .zIndex(this.item.zIndex) + // Transparent 模式确保点到透明处会穿透,点到实体像素才拦截 + .hitTestBehavior(HitTestMode.Transparent) + .visibility(this.item.isHidden ? Visibility.None : Visibility.Visible) + .gesture( + GestureGroup(GestureMode.Parallel, + PanGesture() + .onActionStart(() => { + // 手指摸到的瞬间,立刻把选中的人设为自己 + this.selectedIndex = this.index; + // 确保选中的在同类最前面 + this.promoteZIndex(); + }) + .onActionUpdate((event: GestureEvent) => { + if (this.selectedIndex !== this.index) return; + this.item.posX = this.item.lastX + event.offsetX; + this.item.posY = this.item.lastY + event.offsetY; + }) + .onActionEnd(() => { + this.item.lastX = this.item.posX; + this.item.lastY = this.item.posY; + }), + // 缩放和旋转手势同理在 onActionStart 加入选中逻辑 + PinchGesture() + .onActionStart(() => { this.selectedIndex = this.index; }) + .onActionUpdate((event: GestureEvent) => { + if (this.selectedIndex !== this.index) return; + this.item.scaleValue = this.item.lastScale * event.scale; + }) + .onActionEnd(() => { this.item.lastScale = this.item.scaleValue; }), + + RotationGesture() + .onActionStart(() => { this.selectedIndex = this.index; }) + .onActionUpdate((event: GestureEvent) => { + if (this.selectedIndex !== this.index) return; + this.item.rotateValue = this.item.lastRotate + event.angle; + }) + .onActionEnd(() => { this.item.lastRotate = this.item.rotateValue; }) + ) + ) + } + + private promoteZIndex() { + const baseZ = (this.item.type + 1) * 10; + this.item.zIndex = baseZ + 9; // 直接拉到该区间的最高位 + } +} + + diff --git a/features/mainLayout/src/main/ets/view/TitleBarComponent.ets b/features/mainLayout/src/main/ets/view/TitleBarComponent.ets new file mode 100644 index 0000000..791fb9a --- /dev/null +++ b/features/mainLayout/src/main/ets/view/TitleBarComponent.ets @@ -0,0 +1,76 @@ +import { Logger } from "@ohos/common"; +import { router } from "@kit.ArkUI"; + +/** + * 通用TitleBar component. + */ +@Component +export struct TitleBarComponent { + @State titleName: string = ''; + @State isShowTitle: boolean = true; + @State isShowBack: boolean = true; + @State isShowSave: boolean = false; + @StorageProp('statusBarHeight') statusBarHeight: number = 0; + @Consume('pageInfos') pageStack: NavPathStack; + + onSave?: () => void; + + build() { + Column() { + //标题栏 + Row() { + if(this.isShowBack){ + Image($r('app.media.ic_back_arrow')) + .width('24vp') + .height('24vp') + .onClick(() => { + // 1. 先尝试从 NavPathStack 返回(针对 Navigation 内部子页) + if (this.pageStack.size() > 0) { + this.pageStack.pop(); + Logger.info('TitleBarComponent', 'NavStack 返回'); + } else { + // 2. 如果栈为空,说明是 router 页面,执行 router 返回 + router.back(); + Logger.info('TitleBarComponent', 'Router 返回'); + } + }) + } + + Stack(){ + if(this.isShowTitle){ + Text(this.titleName) + .fontSize(16) + .fontColor('#FF1A1A1A') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + } + .layoutWeight(1) + .alignContent(Alignment.Center) + + if(this.isShowSave){ + Button('保存') + .width('60vp') + .height('30vp') + .fontSize(14) + .fontColor('#FFC2FF43') + .borderRadius(359) + .backgroundColor('#FF000000') + .onClick(() => { + // 保存 + if (this.onSave) { + this.onSave(); + } + }) + .align(Alignment.End) + } + } + .width('100%') + .height('56vp') + .margin({ top: this.statusBarHeight}) + .alignItems(VerticalAlign.Center) + .padding({ left: 16, right: 16 }) + } + } +} \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/view/ZoomView.ets b/features/mainLayout/src/main/ets/view/ZoomView.ets new file mode 100644 index 0000000..47b328b --- /dev/null +++ b/features/mainLayout/src/main/ets/view/ZoomView.ets @@ -0,0 +1,51 @@ +import { StickerItem } from '@ohos/common/src/main/ets/viewmodel/LocalBean'; + +@Component +export struct ZoomView { + @ObjectLink item: StickerItem; + + build() { + Image(this.item.pixelMap) + .width('100%') + .height('100%') + .objectFit(ImageFit.Contain) + .translate({ x: this.item.posX, y: this.item.posY }) + .scale({ x: this.item.scaleValue, y: this.item.scaleValue }) + .zIndex(this.item.zIndex) + .hitTestBehavior(HitTestMode.Transparent) + .visibility(this.item.isHidden ? Visibility.None : Visibility.Visible) + .gesture( + GestureGroup(GestureMode.Parallel, + PanGesture() + .onActionStart(() => { + this.promoteZIndex(); + }) + .onActionUpdate((event: GestureEvent) => { + this.item.posX = this.item.lastX + event.offsetX; + this.item.posY = this.item.lastY + event.offsetY; + }) + .onActionEnd(() => { + this.item.lastX = this.item.posX; + this.item.lastY = this.item.posY; + }), + + PinchGesture() + .onActionUpdate((event: GestureEvent) => { + this.item.scaleValue = this.item.lastScale * event.scale; + }) + .onActionEnd(() => { + this.item.lastScale = this.item.scaleValue; + }) + + ) + ) + } + + private promoteZIndex() { + const baseZ = (this.item.type + 1) * 10; + this.item.zIndex = baseZ + 9; + } +} + + + diff --git a/features/mainLayout/src/main/ets/viewmodel/CuteViewModel.ets b/features/mainLayout/src/main/ets/viewmodel/CuteViewModel.ets new file mode 100644 index 0000000..1badb4a --- /dev/null +++ b/features/mainLayout/src/main/ets/viewmodel/CuteViewModel.ets @@ -0,0 +1,27 @@ +import { image } from "@kit.ImageKit"; + +/** + * 抠图页ViewModel + */ +export class CuteViewModel { + private pixelMap: image.PixelMap | null = null; + + setProcessedPixelMap(pm: image.PixelMap) { + this.pixelMap = pm; + } + + getProcessedPixelMap(): image.PixelMap | null { + return this.pixelMap; + } + + release() { + if (this.pixelMap) { + this.pixelMap.release(); + this.pixelMap = null; + } + } +} + +const cuteViewModel = new CuteViewModel(); + +export default cuteViewModel; \ No newline at end of file diff --git a/features/mainLayout/src/main/ets/viewmodel/DataBean.ets b/features/mainLayout/src/main/ets/viewmodel/DataBean.ets new file mode 100644 index 0000000..1535124 --- /dev/null +++ b/features/mainLayout/src/main/ets/viewmodel/DataBean.ets @@ -0,0 +1,31 @@ +// 文件上传信息 +export interface UploadInfo { + isSuccess: boolean; + message: string; + data: ImageInfo | undefined; +} + +// 图片信息 +export interface ImageInfo { + id: string; + url: string; + base64: string; + expire: string; + file_type: string; +} + +/** + * ServiceProfile 的专属转换器 + */ +export const UserProfileMapper = (raw: Record): ServiceProfile => { + return { + corpid: (raw['corpid'] as string) ?? "", + address: (raw['kf.address'] as string) ?? '' + }; +}; + +// 客服信息 +export interface ServiceProfile { + corpid: string; + address: string; +} diff --git a/features/mainLayout/src/main/ets/viewmodel/LocalBean.ets b/features/mainLayout/src/main/ets/viewmodel/LocalBean.ets new file mode 100644 index 0000000..17d921b --- /dev/null +++ b/features/mainLayout/src/main/ets/viewmodel/LocalBean.ets @@ -0,0 +1,26 @@ +// Demo数据 +export interface DemoItem { + id: string; + name: string; // 显示的文本内容 + icon: Resource; // 可选的图标资源 +} + +export interface TrimStyle { + id: string; + type: number; // 0:女,1:男 + name: string; // 显示的文本内容 + icon: Resource; // 可选的图标资源 +} +export interface ResizeStyle { + id: string; + name: string; // 显示的文本内容 + width: number; // 宽 + height: number; // 高 +} + +export interface FormatStyle { + id: string; + type: number; // 0:JPG,1:PNG 2: GIF 3:SVG + name: string; // 显示的文本内容 +} + diff --git a/features/mainLayout/src/main/ets/viewmodel/MainService.ets b/features/mainLayout/src/main/ets/viewmodel/MainService.ets new file mode 100644 index 0000000..d955559 --- /dev/null +++ b/features/mainLayout/src/main/ets/viewmodel/MainService.ets @@ -0,0 +1,469 @@ +import { ImageInfo, ServiceProfile, UserProfileMapper } from "./DataBean"; +import { ApiManager } from '@ohos/common/src/main/ets/provider/ApiManager'; +import { FormData } from '@ohos/axios'; +import { BaseResponse } from "@ohos/common/src/main/ets/provider/BaseResponse"; +import { RequestCallback } from "@ohos/common/src/main/ets/provider/Callback"; +import { fileUri } from '@kit.CoreFileKit'; +import { FeedbackRequest } from "./Request"; +import { LoginProfile, UserInfo } from "@ohos/common/src/main/ets/viewmodel/DataBean"; +import { Logger } from "@ohos/common"; +import { KVStore } from "@ohos/common/src/main/ets/utils/KVStore"; +import { LOGIN_PROFILE, TOKEN, TOKEN_LOGIN } from "@ohos/common/src/main/ets/constants/AppConstants"; + +export class MainService { + /** + * 文件上传(反馈图片) + * @param fileUri + */ + static async uploadFile(fileUri: string, callback?: RequestCallback) { + try { + // 1. 准备数据 + const fd = new FormData(); + fd.append('file', fileUri); + fd.append('type', 'feedback'); + + // 2. 调用封装好的 request + const result = await ApiManager.getInstance().request( + '/api/user/upload', + undefined, + 'POST', + fd + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } catch (error) { + if (callback && callback.onError) { + callback.onError(error as Error); + } + } + } + + /** + * 图片上传(反馈图片,目前受阻于签名) + * @param uri 图片URI + * @param callback + */ + static async uploadImage(uri: string, callback?: RequestCallback) { + try { + //拿到uri后构建FormData参数,此处有两种方式 + //第一种,获取直接放沙箱路径,axios支持这种 + const fileUriObject = new fileUri.FileUri(uri) + const path = fileUriObject.path + const name = MainService.getFileNameFromUri(uri); + + //第二种是internal://协议的地址 + // const filePath: string = uri; + // const resFile = fs.openSync(filePath, fs.OpenMode.READ_ONLY) + // let newPath = getContext().cacheDir + "/" + resFile.name; + // fs.copyFileSync(resFile.fd, newPath) + // const path = "internal://cache/" + resFile.name; + + let formData = new FormData() + formData.append('file', path, { filename: name, type: 'image/jpeg' }) + + const result = await ApiManager.getInstance().upload>( + '/api/user/upload', + formData, + { "scene": "feedback" } + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } catch (error) { + if (callback && callback.onError) { + callback.onError(error as Error); + } + } + } + + /** + * 图片上传(反馈图片) + * @param base64 + * @param callback + */ + static async uploadImageBase64(base64: string, callback?: RequestCallback) { + try { + const postData: Record = { + 'file': base64 + } + const params: Record = { + 'type': 'base64', + 'scene': 'feedback' + } + + // 3. 发送请求 (使用你 ApiManager 的 request 方法) + const result = await ApiManager.getInstance().request( + '/api/user/upload', + params, + 'post', + postData + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } catch (error) { + if (callback && callback.onError) { + callback.onError(error as Error); + } + } + } + + /** + * 图片删除 + * @param id + * @param callback + */ + static async deleteImage(id: string, callback?: RequestCallback){ + try { + const postData: Record = { + 'id': id + } + const result = await ApiManager.getInstance().request( + '/api/user/upload', + undefined, + 'delete', + postData + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } catch (error) { + if (callback && callback.onError) { + callback.onError(error as Error); + } + } + } + + /** + * 用户反馈 + * @param id + * @param callback + */ + static async feedback(type: string, content:string, contact: string, images: string[], callback?: RequestCallback){ + try { + const postData: FeedbackRequest = { + 'type': type, + 'content': content, + 'contact': contact, + 'images': images + }; + const result = await ApiManager.getInstance().request( + '/api/user/feedback', + undefined, + 'post', + postData + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } catch (error) { + if (callback && callback.onError) { + callback.onError(error as Error); + } + } + } + + /** + * 联系客服(获取客服连接) + */ + static async getService(): Promise { + const result = await ApiManager.getInstance().requestForMapper( + '/api/weixin/service', + undefined, // 传参,如有则传入 + UserProfileMapper // 映射,数据映射器 + ); + + return result; + } + + /** + * 获取账号列表 + */ + static async getAccount(): Promise> { + const queryParams: Record = { + 'scene': 'account' + }; + const result = await ApiManager.getInstance().request>( + '/api/user/account', + queryParams, // 传参,如有则传入 + undefined // 映射,数据映射器 + ); + + return result.data; + } + + /** + * 用户注销 + * @param callback + */ + static async deleteAccount(callback?: RequestCallback){ + try { + const result = await ApiManager.getInstance().request( + '/api/user/destroy', + undefined, + 'post' + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } catch (error) { + if (callback && callback.onError) { + callback.onError(error as Error); + } + } + } + + /** + * 绑定:验证码绑定 + * @param phone + * @param callback + */ + static async bindForCaptcha(phone: string, captchaCode: string, captchaTimestamp: string, callback?: RequestCallback) { + try { + const jsonPhone: Record = { + "timestamp": captchaTimestamp, + "phone": phone, + "code": captchaCode + }; + const postData: Record = { + "type": "phone", + "bind": "1", + "data": jsonPhone + }; + + MainService.bind(postData, callback) + } catch (err) { + if (callback && callback.onError) { + callback.onError(err as Error); + } + } + } + + /** + * 解绑:手机号 + * @param phone + * @param callback + */ + static async unBindPhone(phone: string, callback?: RequestCallback) { + try { + const jsonPhone: Record = { + "phone": phone, + }; + const postData: Record = { + "type": "phone", + "bind": "2", + "data": jsonPhone + }; + + MainService.bind(postData, callback) + } catch (err) { + if (callback && callback.onError) { + callback.onError(err as Error); + } + } + } + + /** + * 绑定:微信 + * @param authCode + * @param callback + */ + static async wxBind(authCode: string, callback?: RequestCallback) { + try { + const jsonWxAuth: Record = { + "code": authCode, + "code_type": '' + }; + const postData: Record = { + "type": "weixin", + "bind": "1", + "data": jsonWxAuth + }; + + MainService.bind(postData, callback) + } catch (err) { + if (callback && callback.onError) { + callback.onError(err as Error); + } + } + } + + /** + * 解绑:微信 + * @param wechatCode + * @param callback + */ + static async unBindWechat(wechatCode: string, callback?: RequestCallback) { + try { + const jsonWechat: Record = { + "code": wechatCode, + "code_type": '' + }; + const postData: Record = { + "type": "weixin", + "bind": "2", + "data": jsonWechat + }; + + MainService.bind(postData, callback) + } catch (err) { + if (callback && callback.onError) { + callback.onError(err as Error); + } + } + } + + /** + * 绑定/解绑:适用于手机验证、一键和微信 + * @param phone + * @param callback + */ + static async bind(data: Record, callback?: RequestCallback) { + Logger.info('loginInteface', 'wxBind: ------------->' + JSON.stringify(data)); + let result = await ApiManager.getInstance().request( + '/api/user/login', + undefined, + 'post', + data + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } + + /** + * 切换账号 + * @param userId + * @param callback + */ + static async switchAccount(userId: string, callback?: RequestCallback) { + try { + const jsonSwitch: Record = { + "user_id": userId + }; + const data: Record = { + "type": "device", + "data": jsonSwitch + }; + + let result = await ApiManager.getInstance().request( + '/api/user/login', + undefined, + 'post', + data + ); + + 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); + } + } + } + + /** + * 退出登录 + */ + static async logout(callback?: RequestCallback) { + let result = await ApiManager.getInstance().request( + '/api/user/logout', + undefined, + 'post' + ); + + if(result.code === 200 || result.code === 0){ + if (callback && callback.onSuccess) { + callback.onSuccess(result.data); + } + }else{ + if (callback && callback.onFailure) { + callback.onFailure(result.message); + } + } + } + + + /** + * 从 URI 中获取文件名 + * @param uri + * @returns + */ + static getFileNameFromUri(uri: string): string { + if (!uri) return 'unknown_file'; + + // 兼容斜杠 / 和反斜杠 \ + const parts = uri.split(/[\\\/]/); + const fileName = parts[parts.length - 1]; + + // 如果 URI 以 / 结尾,处理一下 + return fileName || 'default_name'; + } + +} diff --git a/features/mainLayout/src/main/ets/viewmodel/Request.ets b/features/mainLayout/src/main/ets/viewmodel/Request.ets new file mode 100644 index 0000000..9c3b705 --- /dev/null +++ b/features/mainLayout/src/main/ets/viewmodel/Request.ets @@ -0,0 +1,6 @@ +export interface FeedbackRequest { + type: string; + content: string; + contact: string; + images: string[]; // 支持字符串数组 +} \ No newline at end of file diff --git a/features/mainLayout/src/main/module.json5 b/features/mainLayout/src/main/module.json5 new file mode 100644 index 0000000..9a6bad6 --- /dev/null +++ b/features/mainLayout/src/main/module.json5 @@ -0,0 +1,15 @@ +{ + "module": { + "name": "mainLayout", + "type": "har", + "deviceTypes": [ + "phone" + ], + "routerMap": "$profile:route_map", + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET", + } + ] + } +} \ No newline at end of file diff --git a/features/mainLayout/src/main/resources/base/element/color.json b/features/mainLayout/src/main/resources/base/element/color.json new file mode 100644 index 0000000..e80a090 --- /dev/null +++ b/features/mainLayout/src/main/resources/base/element/color.json @@ -0,0 +1,20 @@ +{ + "color": [ + { + "name": "tab_font_color_normal", + "value": "#A7A7A7" + }, + { + "name": "tab_font_color_selected", + "value": "#333333" + }, + { + "name": "tab_line", + "value": "#4DA7A7A7" + }, + { + "name": "normal_background", + "value": "#FFFFFFFF" + } + ] +} \ No newline at end of file diff --git a/features/mainLayout/src/main/resources/base/element/float.json b/features/mainLayout/src/main/resources/base/element/float.json new file mode 100644 index 0000000..aaa0290 --- /dev/null +++ b/features/mainLayout/src/main/resources/base/element/float.json @@ -0,0 +1,103 @@ +{ + "float": [ + { + "name": "tab_text_font_size", + "value": "10vp" + }, + { + "name": "arrow_width", + "value": "24vp" + }, + { + "name": "arrow_height", + "value": "24vp" + }, + { + "name": "arrow_margin_left", + "value": "24vp" + }, + { + "name": "arrow_left_borderRadius", + "value": "24vp" + }, + { + "name": "arrow_item_height", + "value": "56vp" + }, + { + "name": "grid_margin_left", + "value": "24vp" + }, + { + "name": "grid_margin_right", + "value": "24vp" + }, + { + "name": "grid_padding_bottom", + "value": "80vp" + }, + { + "name": "grid_column_gap", + "value": "12vp" + }, + { + "name": "grid_row_gap", + "value": "12vp" + }, + { + "name": "grid_text_top", + "value": "8vp" + }, { + "name": "grid_text_font_size", + "value": "14vp" + }, + { + "name": "grid_text_padding_left", + "value": "4vp" + }, + { + "name": "grid_row_border_radius", + "value": "12vp" + }, + { + "name": "tab_img_width", + "value": "24vp" + }, + { + "name": "tab_img_height", + "value": "24vp" + }, + { + "name": "tab_text_sm_top", + "value": "4vp" + }, + { + "name": "tab_text_sm_left", + "value": "0vp" + }, + { + "name": "tab_text_md_top", + "value": "0vp" + }, + { + "name": "tab_text_md_left", + "value": "10vp" + }, + { + "name": "tab_text_lg_top", + "value": "4vp" + }, + { + "name": "tab_text_lg_left", + "value": "0vp" + }, + { + "name": "tab_text_xl_top", + "value": "4vp" + }, + { + "name": "tab_text_xl_left", + "value": "0vp" + } + ] +} \ No newline at end of file diff --git a/features/mainLayout/src/main/resources/base/element/string.json b/features/mainLayout/src/main/resources/base/element/string.json new file mode 100644 index 0000000..c70d66d --- /dev/null +++ b/features/mainLayout/src/main/resources/base/element/string.json @@ -0,0 +1,44 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "主要模块,大多数页面均使用此模块" + }, + { + "name": "ResponsiveLayoutItemAbility_desc", + "value": "description" + }, + { + "name": "ResponsiveLayoutItemAbility_label", + "value": "截图兔" + }, + { + "name": "grid_title_label", + "value": "title" + }, + { + "name": "tab_home", + "value": "首页" + }, + { + "name": "tab_mine", + "value": "我的" + }, + { + "name": "layout_home", + "value": "首页" + }, + { + "name": "layout_mine", + "value": "我的" + }, + { + "name": "layout_mine_feedback", + "value": "意见反馈" + }, + { + "name": "layout_mine_setting", + "value": "设置" + } + ] +} \ No newline at end of file diff --git a/features/mainLayout/src/main/resources/base/media/ic_app_logo.png b/features/mainLayout/src/main/resources/base/media/ic_app_logo.png new file mode 100644 index 0000000..8f8a39a Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_app_logo.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_back_arrow.png b/features/mainLayout/src/main/resources/base/media/ic_back_arrow.png new file mode 100644 index 0000000..927d554 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_back_arrow.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_close.png b/features/mainLayout/src/main/resources/base/media/ic_close.png new file mode 100644 index 0000000..3cc77f6 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_close.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_female_1.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_1.png new file mode 100644 index 0000000..e412913 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_1.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_female_2.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_2.png new file mode 100644 index 0000000..e40c7cf Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_2.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_female_3.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_3.png new file mode 100644 index 0000000..0668071 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_3.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_female_4.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_4.png new file mode 100644 index 0000000..4c6f814 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_4.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_female_5.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_5.png new file mode 100644 index 0000000..6260250 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_5.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_female_6.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_6.png new file mode 100644 index 0000000..dfc58ec Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_female_6.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_1.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_1.png new file mode 100644 index 0000000..5fb5a90 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_1.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_2.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_2.png new file mode 100644 index 0000000..ff1df2f Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_2.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_3.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_3.png new file mode 100644 index 0000000..587b5b9 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_3.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_4.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_4.png new file mode 100644 index 0000000..5397082 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_4.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_5.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_5.png new file mode 100644 index 0000000..d09840e Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_5.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_6.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_6.png new file mode 100644 index 0000000..d116699 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_6.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_clothing_man_7.png b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_7.png new file mode 100644 index 0000000..6c44d60 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_clothing_man_7.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_cute_background_color_normal.png b/features/mainLayout/src/main/resources/base/media/ic_cute_background_color_normal.png new file mode 100644 index 0000000..99c2772 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_cute_background_color_normal.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_cute_background_icon.png b/features/mainLayout/src/main/resources/base/media/ic_cute_background_icon.png new file mode 100644 index 0000000..32162fa Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_cute_background_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_cute_clothing_icon.png b/features/mainLayout/src/main/resources/base/media/ic_cute_clothing_icon.png new file mode 100644 index 0000000..68c2152 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_cute_clothing_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_cute_hairstyle_icon.png b/features/mainLayout/src/main/resources/base/media/ic_cute_hairstyle_icon.png new file mode 100644 index 0000000..67ea753 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_cute_hairstyle_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_cute_resize_icon.png b/features/mainLayout/src/main/resources/base/media/ic_cute_resize_icon.png new file mode 100644 index 0000000..484c6af Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_cute_resize_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_guide_example.png b/features/mainLayout/src/main/resources/base/media/ic_guide_example.png new file mode 100644 index 0000000..e3e587b Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_guide_example.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_guide_watermark.png b/features/mainLayout/src/main/resources/base/media/ic_guide_watermark.png new file mode 100644 index 0000000..0136959 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_guide_watermark.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_1.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_1.png new file mode 100644 index 0000000..7b50c24 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_1.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_2.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_2.png new file mode 100644 index 0000000..d4b7b8d Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_2.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_3.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_3.png new file mode 100644 index 0000000..9c1f809 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_3.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_4.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_4.png new file mode 100644 index 0000000..68bb66e Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_4.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_5.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_5.png new file mode 100644 index 0000000..dfa6830 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_5.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_6.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_6.png new file mode 100644 index 0000000..413ce09 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_female_6.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_1.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_1.png new file mode 100644 index 0000000..c6dba1a Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_1.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_2.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_2.png new file mode 100644 index 0000000..284d32a Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_2.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_3.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_3.png new file mode 100644 index 0000000..b9ca718 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_3.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_4.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_4.png new file mode 100644 index 0000000..98c3a59 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_4.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_5.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_5.png new file mode 100644 index 0000000..a57d95b Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_5.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_6.png b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_6.png new file mode 100644 index 0000000..05e2068 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_hairstyle_man_6.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_certificate_title_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_certificate_title_icon.png new file mode 100644 index 0000000..78dec98 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_certificate_title_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_cute_title_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_cute_title_icon.png new file mode 100644 index 0000000..e08a85e Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_cute_title_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_format_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_format_icon.png new file mode 100644 index 0000000..3635e5f Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_format_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_long_image_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_long_image_icon.png new file mode 100644 index 0000000..d603f3b Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_long_image_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_other_title.png b/features/mainLayout/src/main/resources/base/media/ic_home_other_title.png new file mode 100644 index 0000000..e1cebab Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_other_title.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_photo_guide_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_photo_guide_icon.png new file mode 100644 index 0000000..53baa76 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_photo_guide_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_size_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_size_icon.png new file mode 100644 index 0000000..887a469 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_size_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_top_image.png b/features/mainLayout/src/main/resources/base/media/ic_home_top_image.png new file mode 100644 index 0000000..f9d9407 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_top_image.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_home_unimp_title_icon.png b/features/mainLayout/src/main/resources/base/media/ic_home_unimp_title_icon.png new file mode 100644 index 0000000..616aea7 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_home_unimp_title_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_image_add.png b/features/mainLayout/src/main/resources/base/media/ic_image_add.png new file mode 100644 index 0000000..065164e Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_image_add.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_image_pld.png b/features/mainLayout/src/main/resources/base/media/ic_image_pld.png new file mode 100644 index 0000000..5b50294 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_image_pld.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_input_div_line.png b/features/mainLayout/src/main/resources/base/media/ic_login_input_div_line.png new file mode 100644 index 0000000..2b2335c Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_input_div_line.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_mask.png b/features/mainLayout/src/main/resources/base/media/ic_login_mask.png new file mode 100644 index 0000000..0fee5a3 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_mask.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_other_left_line.png b/features/mainLayout/src/main/resources/base/media/ic_login_other_left_line.png new file mode 100644 index 0000000..14c1d06 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_other_left_line.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_other_right_line.png b/features/mainLayout/src/main/resources/base/media/ic_login_other_right_line.png new file mode 100644 index 0000000..ce6b471 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_other_right_line.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_phone_icon.png b/features/mainLayout/src/main/resources/base/media/ic_login_phone_icon.png new file mode 100644 index 0000000..145e082 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_phone_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_vertical_line.png b/features/mainLayout/src/main/resources/base/media/ic_login_vertical_line.png new file mode 100644 index 0000000..3d3992f Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_vertical_line.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_wx_auth_icon.png b/features/mainLayout/src/main/resources/base/media/ic_login_wx_auth_icon.png new file mode 100644 index 0000000..de940ed Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_wx_auth_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_login_wx_icon.png b/features/mainLayout/src/main/resources/base/media/ic_login_wx_icon.png new file mode 100644 index 0000000..2422195 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_login_wx_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_look.png b/features/mainLayout/src/main/resources/base/media/ic_look.png new file mode 100644 index 0000000..ae4c03b Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_look.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_account_mask.png b/features/mainLayout/src/main/resources/base/media/ic_mine_account_mask.png new file mode 100644 index 0000000..d70e63b Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_account_mask.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_arrow_right.png b/features/mainLayout/src/main/resources/base/media/ic_mine_arrow_right.png new file mode 100644 index 0000000..084593b Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_arrow_right.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_copy.png b/features/mainLayout/src/main/resources/base/media/ic_mine_copy.png new file mode 100644 index 0000000..d7d583c Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_copy.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_customer.png b/features/mainLayout/src/main/resources/base/media/ic_mine_customer.png new file mode 100644 index 0000000..a133e8e Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_customer.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_feedback.png b/features/mainLayout/src/main/resources/base/media/ic_mine_feedback.png new file mode 100644 index 0000000..6a17c39 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_feedback.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_setting.png b/features/mainLayout/src/main/resources/base/media/ic_mine_setting.png new file mode 100644 index 0000000..6256c2f Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_setting.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_top_mask_layer.png b/features/mainLayout/src/main/resources/base/media/ic_mine_top_mask_layer.png new file mode 100644 index 0000000..adc4403 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_top_mask_layer.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_update.png b/features/mainLayout/src/main/resources/base/media/ic_mine_update.png new file mode 100644 index 0000000..2ec9bbc Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_update.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_mine_user_normal.png b/features/mainLayout/src/main/resources/base/media/ic_mine_user_normal.png new file mode 100644 index 0000000..8532155 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_mine_user_normal.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_placeholder.png b/features/mainLayout/src/main/resources/base/media/ic_placeholder.png new file mode 100644 index 0000000..7eb2aa8 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_placeholder.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_public_back.svg b/features/mainLayout/src/main/resources/base/media/ic_public_back.svg new file mode 100644 index 0000000..8083427 --- /dev/null +++ b/features/mainLayout/src/main/resources/base/media/ic_public_back.svg @@ -0,0 +1,15 @@ + + + icon_more备份 + + + + + + + + + + + + \ No newline at end of file diff --git a/features/mainLayout/src/main/resources/base/media/ic_resize_icon.png b/features/mainLayout/src/main/resources/base/media/ic_resize_icon.png new file mode 100644 index 0000000..471fbc3 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_resize_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_tabs_home_normal.webp b/features/mainLayout/src/main/resources/base/media/ic_tabs_home_normal.webp new file mode 100644 index 0000000..96678ee Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_tabs_home_normal.webp differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_tabs_home_selected.webp b/features/mainLayout/src/main/resources/base/media/ic_tabs_home_selected.webp new file mode 100644 index 0000000..642f5ea Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_tabs_home_selected.webp differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_tabs_mine_normal.webp b/features/mainLayout/src/main/resources/base/media/ic_tabs_mine_normal.webp new file mode 100644 index 0000000..dba830a Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_tabs_mine_normal.webp differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_tabs_mine_selected.webp b/features/mainLayout/src/main/resources/base/media/ic_tabs_mine_selected.webp new file mode 100644 index 0000000..f7497cf Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_tabs_mine_selected.webp differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_tick_circle_normal.png b/features/mainLayout/src/main/resources/base/media/ic_tick_circle_normal.png new file mode 100644 index 0000000..1cc3264 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_tick_circle_normal.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_tick_circle_selected.png b/features/mainLayout/src/main/resources/base/media/ic_tick_circle_selected.png new file mode 100644 index 0000000..86f0652 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_tick_circle_selected.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_title_indicator.png b/features/mainLayout/src/main/resources/base/media/ic_title_indicator.png new file mode 100644 index 0000000..d7cddf6 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_title_indicator.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_un_look.png b/features/mainLayout/src/main/resources/base/media/ic_un_look.png new file mode 100644 index 0000000..9fde56a Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_un_look.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_unimp_alipay_pld.png b/features/mainLayout/src/main/resources/base/media/ic_unimp_alipay_pld.png new file mode 100644 index 0000000..fc38606 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_unimp_alipay_pld.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_unimp_fly_icon.png b/features/mainLayout/src/main/resources/base/media/ic_unimp_fly_icon.png new file mode 100644 index 0000000..7308778 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_unimp_fly_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_unimp_group_icon.png b/features/mainLayout/src/main/resources/base/media/ic_unimp_group_icon.png new file mode 100644 index 0000000..b226fd3 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_unimp_group_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_unimp_payroll_icon.png b/features/mainLayout/src/main/resources/base/media/ic_unimp_payroll_icon.png new file mode 100644 index 0000000..379b0c3 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_unimp_payroll_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_unimp_train_icon.png b/features/mainLayout/src/main/resources/base/media/ic_unimp_train_icon.png new file mode 100644 index 0000000..ab4a8e0 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_unimp_train_icon.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_unimp_wx_pld.png b/features/mainLayout/src/main/resources/base/media/ic_unimp_wx_pld.png new file mode 100644 index 0000000..acc9958 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_unimp_wx_pld.png differ diff --git a/features/mainLayout/src/main/resources/base/media/ic_warning.png b/features/mainLayout/src/main/resources/base/media/ic_warning.png new file mode 100644 index 0000000..70f7bb0 Binary files /dev/null and b/features/mainLayout/src/main/resources/base/media/ic_warning.png differ diff --git a/features/mainLayout/src/main/resources/base/profile/route_map.json b/features/mainLayout/src/main/resources/base/profile/route_map.json new file mode 100644 index 0000000..e33eed7 --- /dev/null +++ b/features/mainLayout/src/main/resources/base/profile/route_map.json @@ -0,0 +1,69 @@ +{ + "routerMap": [ + { + "name": "FeedbackLayout", + "pageSourceFile": "src/main/ets/pages/FeedbackLayout.ets", + "buildFunction": "FeedbackBuilder" + }, + { + "name": "SettingLayout", + "pageSourceFile": "src/main/ets/pages/SettingLayout.ets", + "buildFunction": "SettingBuilder" + }, + { + "name": "AccountBindLayout", + "pageSourceFile": "src/main/ets/pages/AccountBindLayout.ets", + "buildFunction": "AccountBindBuilder" + }, + { + "name": "AccountBindLoginLayout", + "pageSourceFile": "src/main/ets/pages/AccountBindLoginLayout.ets", + "buildFunction": "AccountBindLoginBuilder" + }, + { + "name": "AccountManagerLayout", + "pageSourceFile": "src/main/ets/pages/AccountManagerLayout.ets", + "buildFunction": "AccountManagerBuilder" + }, + { + "name": "AboutUsLayout", + "pageSourceFile": "src/main/ets/pages/AboutUsLayout.ets", + "buildFunction": "AboutUsBuilder" + }, + { + "name": "DeleteAccountLayout", + "pageSourceFile": "src/main/ets/pages/DeleteAccountLayout.ets", + "buildFunction": "DeleteAccountBuilder" + }, + { + "name": "WebLayout", + "pageSourceFile": "src/main/ets/pages/WebLayout.ets", + "buildFunction": "WebLayoutBuilder" + }, + { + "name": "CuteLayout", + "pageSourceFile": "src/main/ets/pages/CuteLayout.ets", + "buildFunction": "CuteBuilder" + }, + { + "name": "LongImageLayout", + "pageSourceFile": "src/main/ets/pages/LongImageLayout.ets", + "buildFunction": "LongImageBuilder" + }, + { + "name": "FormatLayout", + "pageSourceFile": "src/main/ets/pages/FormatLayout.ets", + "buildFunction": "FormatBuilder" + }, + { + "name": "ResizeLayout", + "pageSourceFile": "src/main/ets/pages/ResizeLayout.ets", + "buildFunction": "ResizeBuilder" + }, + { + "name": "GuideLayout", + "pageSourceFile": "src/main/ets/pages/GuideLayout.ets", + "buildFunction": "GuideBuilder" + } + ] +} diff --git a/features/mainLayout/src/test/List.test.ets b/features/mainLayout/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/features/mainLayout/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/features/mainLayout/src/test/LocalUnit.test.ets b/features/mainLayout/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/features/mainLayout/src/test/LocalUnit.test.ets @@ -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); + }); + }); +} \ No newline at end of file diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..fac94d0 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,23 @@ +{ + "modelVersion": "6.0.2", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/hvigorfile.ts b/hvigorfile.ts new file mode 100644 index 0000000..d1c767c --- /dev/null +++ b/hvigorfile.ts @@ -0,0 +1,77 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; +import fs from 'fs'; +import path from 'path'; + +/* +function updateJsonTime() { + const filePath = path.resolve(__dirname, 'build-profile.json5'); + if (fs.existsSync(filePath)) { + const now = new Date(); + const timeStr = `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}${now.getHours()}${now.getMinutes()}`; + const newArtifactName = `rabbit_harmony_${timeStr}_`; + + let content = fs.readFileSync(filePath, 'utf8'); + // 使用正则表达式精准匹配 BUILD_TIME 后的值并替换,匹配模式: "BUILD_TIME": "任意内容" + const updateTimeContent = content.replace(/("BUILD_TIME"\s*:\s*")[^"]*(")/g, `$1${timeStr}$2`); + const newContent = updateTimeContent.replace(/("artifactName"\s*:\s*")[^"]*(")/g, `$1${newArtifactName}$2`); + + if (content !== newContent) { + fs.writeFileSync(filePath, newContent, 'utf8'); + } + } +} +updateJsonTime(); +*/ + +function syncBuildFileNameByJson() { + const filePath = path.resolve(__dirname, 'build-profile.json5'); + if (!fs.existsSync(filePath)) return; + + try { + // 1. 读取并解析 JSON (JSON5 格式通常可以当作普通字符串解析) + const rawContent = fs.readFileSync(filePath, 'utf8'); + + // 使用正则提取关键信息,避免因 JSON5 语法导致的 JSON.parse 失败 + const signingMatch = rawContent.match(/"signingConfig"\s*:\s*"([^"]+)"/); + const signingConfig = signingMatch ? signingMatch[1] : 'debug'; + + let buildMode = 'debug'; + const profile = JSON.parse(rawContent); + + // 查找对于签名下的 buildModeSet 中 name 为 debug 的 debuggable 值,正则逻辑:定位到 name: "debug" 块,寻找其后的 debuggable 状态 + if (signingConfig === 'debug') { + const releaseConfig = profile.app.buildModeSet.find(item => item.name === 'debug'); + const isDebuggable = releaseConfig ? releaseConfig.buildOption.debuggable : true; + buildMode = isDebuggable ? 'debug' : 'release'; + } else if (signingConfig === 'release') { + const releaseConfig = profile.app.buildModeSet.find(item => item.name === 'release'); + const isDebuggable = releaseConfig ? releaseConfig.buildOption.debuggable : true; + buildMode = isDebuggable ? 'debug' : 'release'; + } + + // 2. 生成动态名称 + const now = new Date(); + const timeStr = `${now.getFullYear().toString().slice(-2)}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`; + const newName = `rabbit_harmony_${buildMode}_${timeStr}`; + + // 3. 物理替换并写入 + const updatedArtifactName = rawContent.replace(/("artifactName"\s*:\s*")[^"]*(")/g, `$1${newName}$2`); + + const updateBuildTimeContent = updatedArtifactName.replace(/("BUILD_TIME"\s*:\s*")[^"]*(")/g, `$1${timeStr}$2`); + const updatedContent = updateBuildTimeContent + + if (rawContent !== updatedContent) { + fs.writeFileSync(filePath, updatedContent, 'utf8'); + } + } catch (e) { + console.error('>>>> [Error] 解析 build-profile.json5 失败: ' + e.message); + } +} + +// 脚本加载即执行 +syncBuildFileNameByJson(); + +export default { + system: appTasks, + plugins: [] +}; \ No newline at end of file diff --git a/libs/AFServiceSDK.har b/libs/AFServiceSDK.har new file mode 100644 index 0000000..56b5871 Binary files /dev/null and b/libs/AFServiceSDK.har differ diff --git a/oh-package-lock.json5 b/oh-package-lock.json5 new file mode 100644 index 0000000..989a6a4 --- /dev/null +++ b/oh-package-lock.json5 @@ -0,0 +1,209 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@alipay/afservicesdk@libs/AFServiceSDK.har": "@alipay/afservicesdk@libs/AFServiceSDK.har", + "@alipay/blueshieldsdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/blueshieldsdk-1.0.30.har": "@alipay/blueshieldsdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/blueshieldsdk-1.0.30.har", + "@candies/image_cropper@^1.1.1": "@candies/image_cropper@1.1.1", + "@candies/image_editor@^1.0.0": "@candies/image_editor@1.0.0", + "@cashier_alipay/cashiersdk@15.8.24": "@cashier_alipay/cashiersdk@15.8.43", + "@cashier_alipay/cashiersdk@^15.8.43": "@cashier_alipay/cashiersdk@15.8.43", + "@dcloudio/uni-app-harmony@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-app-harmony": "@dcloudio/uni-app-harmony@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-app-harmony", + "@dcloudio/uni-app-runtime@^4.75.2025071101": "@dcloudio/uni-app-runtime@4.84.2025110301", + "@dcloudio/uni-mp-sdk@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-mp-sdk": "@dcloudio/uni-mp-sdk@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-mp-sdk", + "@ohos/axios@^2.2.8": "@ohos/axios@2.2.8", + "@ohos/crypto-js@2.0.0": "@ohos/crypto-js@2.0.5", + "@ohos/crypto-js@^2.0.4": "@ohos/crypto-js@2.0.5", + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25", + "@ohos/videocompressor@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/libs/videoCompressor.har": "@ohos/videocompressor@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/libs/videoCompressor.har", + "@pura/harmony-utils@^1.4.0": "@pura/harmony-utils@1.4.0", + "@pura/picker_utils@^1.0.2": "@pura/picker_utils@1.0.2", + "@taobao-ohos/utdid_sdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/utdid_sdk-1.2.5.har": "@taobao-ohos/utdid_sdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/utdid_sdk-1.2.5.har", + "@tencent/wechat_open_sdk@^1.0.16": "@tencent/wechat_open_sdk@1.0.16", + "@uni_modules/uni-payment-alipay@^1.0.2": "@uni_modules/uni-payment-alipay@1.0.2", + "libblueshield.so@oh_modules/.ohpm/@alipay+blueshieldsdk@qgf9fllog8tazdv8uop120fy3saz8l+7jb55+sipesk=/oh_modules/@alipay/blueshieldsdk/src/main/cpp/types/libblueshield": "libblueshield.so@oh_modules/.ohpm/@alipay+blueshieldsdk@qgf9fllog8tazdv8uop120fy3saz8l+7jb55+sipesk=/oh_modules/@alipay/blueshieldsdk/src/main/cpp/types/libblueshield", + "libjsruntime.so@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/cpp/types/libjsruntime": "libjsruntime.so@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/cpp/types/libjsruntime", + "libutdid_native.so@oh_modules/.ohpm/@taobao-ohos+utdid_sdk@ecjyoomnzz6zaipo1koz9+avemnpw7+togfp5uwrtiu=/oh_modules/@taobao-ohos/utdid_sdk/src/main/cpp/types/libutdid_native": "libutdid_native.so@oh_modules/.ohpm/@taobao-ohos+utdid_sdk@ecjyoomnzz6zaipo1koz9+avemnpw7+togfp5uwrtiu=/oh_modules/@taobao-ohos/utdid_sdk/src/main/cpp/types/libutdid_native", + "pako@^2.1.0": "pako@2.1.0" + }, + "packages": { + "@alipay/afservicesdk@libs/AFServiceSDK.har": { + "name": "@alipay/afservicesdk", + "version": "1.0.1", + "resolved": "libs/AFServiceSDK.har", + "registryType": "local", + "dependencies": { + "@ohos/crypto-js": "2.0.0" + } + }, + "@alipay/blueshieldsdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/blueshieldsdk-1.0.30.har": { + "name": "@alipay/blueshieldsdk", + "version": "1.0.30", + "resolved": "oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/blueshieldsdk-1.0.30.har", + "registryType": "local", + "dependencies": { + "libblueshield.so": "file:./src/main/cpp/types/libblueshield" + } + }, + "@candies/image_cropper@1.1.1": { + "name": "@candies/image_cropper", + "version": "1.1.1", + "integrity": "sha512-vNbzWkGGYRASiRIyS+91O4wrGmt2/h1RyLKW1+PD5G9KjZOJkv7ahjwdI/syNGLDG7aS5Yvk+XlHfqt3jddz8g==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@candies/image_cropper/-/image_cropper-1.1.1.har", + "registryType": "ohpm", + "dependencies": { + "@candies/image_editor": "^1.0.0" + } + }, + "@candies/image_editor@1.0.0": { + "name": "@candies/image_editor", + "version": "1.0.0", + "integrity": "sha512-xYFQeR4FHYlzUrpPSgnKAXaydWfNaRwM12uPBqBdLQzpNL8OWIRcexL1Z9V7jm0OvKlIgVSr7VWW6c4mFKyJPg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@candies/image_editor/-/image_editor-1.0.0.har", + "registryType": "ohpm" + }, + "@cashier_alipay/cashiersdk@15.8.43": { + "name": "@cashier_alipay/cashiersdk", + "version": "15.8.43", + "integrity": "sha512-snzKHkmgrBHC4aWzFdNTZd3n3akdv3mPv71mIwvkzG7vefrMevy9BYx94to9XT3OmHHlIG9YP7jfwgfeBd2t+g==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@cashier_alipay/cashiersdk/-/cashiersdk-15.8.43.har", + "registryType": "ohpm", + "dependencies": { + "@taobao-ohos/utdid_sdk": "file:./lib/utdid_sdk-1.2.5.har", + "@alipay/blueshieldsdk": "file:./lib/blueshieldsdk-1.0.30.har", + "pako": "^2.1.0" + } + }, + "@dcloudio/uni-app-harmony@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-app-harmony": { + "name": "@dcloudio/uni-app-harmony", + "version": "1.0.0", + "resolved": "oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-app-harmony", + "registryType": "local" + }, + "@dcloudio/uni-app-runtime@4.84.2025110301": { + "name": "@dcloudio/uni-app-runtime", + "version": "4.84.2025110301", + "integrity": "sha512-rvU2eXsjGYKQ9rdCh/fcOSI+61iTEpc8UWAzgq1291UTjpIVyM+gk2tc4GUyJpq+Kea44pSrohuetYtu5wQGFQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@dcloudio/uni-app-runtime/-/uni-app-runtime-4.84.2025110301.har", + "registryType": "ohpm", + "dependencies": { + "libjsruntime.so": "./src/main/cpp/types/libjsruntime", + "@dcloudio/uni-app-harmony": "./src/main/ets/uni-app-harmony", + "@dcloudio/uni-mp-sdk": "./src/main/ets/uni-mp-sdk", + "@ohos/videocompressor": "file:./libs/videoCompressor.har" + } + }, + "@dcloudio/uni-mp-sdk@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-mp-sdk": { + "name": "@dcloudio/uni-mp-sdk", + "version": "1.0.0", + "resolved": "oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/ets/uni-mp-sdk", + "registryType": "local" + }, + "@ohos/axios@2.2.8": { + "name": "@ohos/axios", + "version": "2.2.8", + "integrity": "sha512-3qWZN31bY1IMAKNLjIMu2wizXPYNNI/rE8Pmrtgs9z3RxetWhe31HnH4Jb3kBIwaehPlAHgv/It9bTU+8rJ45A==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/axios/-/axios-2.2.8.har", + "registryType": "ohpm" + }, + "@ohos/crypto-js@2.0.5": { + "name": "@ohos/crypto-js", + "version": "2.0.5", + "integrity": "sha512-QPW+vxSambzep39qnnvGLk2lItqPqt+HBTo7FsTdL9edv3z35h4D80ClnEPNPfXQzIp8DgVDMIgbAWa1hWq8ug==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/crypto-js/-/crypto-js-2.0.5.har", + "registryType": "ohpm" + }, + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.25": { + "name": "@ohos/hypium", + "version": "1.0.25", + "integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har", + "registryType": "ohpm" + }, + "@ohos/videocompressor@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/libs/videoCompressor.har": { + "name": "@ohos/videocompressor", + "version": "1.0.4", + "resolved": "oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/libs/videoCompressor.har", + "registryType": "local" + }, + "@pura/harmony-utils@1.4.0": { + "name": "@pura/harmony-utils", + "version": "1.4.0", + "integrity": "sha512-nxEYuXZCQHY+ycjsJ3u6oHCsJMlXfYePhbTz+k0yAn6AzvqvuhbJcUpZj02CBF4ym/1jXICsDVcHKnQGoVMGaw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@pura/harmony-utils/-/harmony-utils-1.4.0.har", + "registryType": "ohpm" + }, + "@pura/picker_utils@1.0.2": { + "name": "@pura/picker_utils", + "version": "1.0.2", + "integrity": "sha512-m4YO7BTtYhCq0ITTXHGh45206JxnBmBB5067PVUhbqqBeZ7OC8Cror++f8OW2Q/clIIvps82h4LX5fx2sBOhQw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@pura/picker_utils/-/picker_utils-1.0.2.har", + "registryType": "ohpm" + }, + "@taobao-ohos/utdid_sdk@oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/utdid_sdk-1.2.5.har": { + "name": "@taobao-ohos/utdid_sdk", + "version": "1.2.5", + "resolved": "oh_modules/.ohpm/@cashier_alipay+cashiersdk@15.8.43/oh_modules/@cashier_alipay/cashiersdk/lib/utdid_sdk-1.2.5.har", + "registryType": "local", + "dependencies": { + "libutdid_native.so": "file:./src/main/cpp/types/libutdid_native", + "@ohos/crypto-js": "^2.0.4" + } + }, + "@tencent/wechat_open_sdk@1.0.16": { + "name": "@tencent/wechat_open_sdk", + "version": "1.0.16", + "integrity": "sha512-/vqcsHFQlQ1UWybijbRTWlT+9BqRUcE/FWsYayEoVwreOeGJ/pdxNHZFkU0EManYRxnqzLoL4SB8HGJNdsm9TA==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencent/wechat_open_sdk/-/wechat_open_sdk-1.0.16.har", + "registryType": "ohpm" + }, + "@uni_modules/uni-payment-alipay@1.0.2": { + "name": "@uni_modules/uni-payment-alipay", + "version": "1.0.2", + "integrity": "sha512-nI1N1ydXTHoh6iRB8CL2uyggfrDPcE6sKaFOJy2HYLHk3sIhLVGBkO0IAN1RgrogPLvID8jiw69CfMnNpMPv8Q==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@uni_modules/uni-payment-alipay/-/uni-payment-alipay-1.0.2.har", + "registryType": "ohpm", + "dependencies": { + "@cashier_alipay/cashiersdk": "15.8.24" + } + }, + "libblueshield.so@oh_modules/.ohpm/@alipay+blueshieldsdk@qgf9fllog8tazdv8uop120fy3saz8l+7jb55+sipesk=/oh_modules/@alipay/blueshieldsdk/src/main/cpp/types/libblueshield": { + "name": "libblueshield.so", + "version": "1.0.8", + "resolved": "oh_modules/.ohpm/@alipay+blueshieldsdk@qgf9fllog8tazdv8uop120fy3saz8l+7jb55+sipesk=/oh_modules/@alipay/blueshieldsdk/src/main/cpp/types/libblueshield", + "registryType": "local" + }, + "libjsruntime.so@oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/cpp/types/libjsruntime": { + "name": "libjsruntime.so", + "version": "1.0.0", + "resolved": "oh_modules/.ohpm/@dcloudio+uni-app-runtime@4.84.2025110301/oh_modules/@dcloudio/uni-app-runtime/src/main/cpp/types/libjsruntime", + "registryType": "local" + }, + "libutdid_native.so@oh_modules/.ohpm/@taobao-ohos+utdid_sdk@ecjyoomnzz6zaipo1koz9+avemnpw7+togfp5uwrtiu=/oh_modules/@taobao-ohos/utdid_sdk/src/main/cpp/types/libutdid_native": { + "name": "libutdid_native.so", + "version": "0.0.1", + "resolved": "oh_modules/.ohpm/@taobao-ohos+utdid_sdk@ecjyoomnzz6zaipo1koz9+avemnpw7+togfp5uwrtiu=/oh_modules/@taobao-ohos/utdid_sdk/src/main/cpp/types/libutdid_native", + "registryType": "local" + }, + "pako@2.1.0": { + "name": "pako", + "version": "2.1.0", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "resolved": "https://ohpm.openharmony.cn/ohpm/pako/-/pako-2.1.0.tgz", + "shasum": "266cc37f98c7d883545d11335c00fbd4062c9a86", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000..078bf93 --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,20 @@ +{ + "modelVersion": "6.0.2", + "description": "Please describe the basic information.", + "dependencies": { + "@ohos/axios": "^2.2.8", + "@pura/harmony-utils": "^1.4.0", + "@pura/picker_utils": "^1.0.2", + "@candies/image_cropper": "^1.1.1", + "@tencent/wechat_open_sdk": "^1.0.16", + "@dcloudio/uni-app-runtime": "^4.75.2025071101", + "@alipay/afservicesdk": "file:./libs/AFServiceSDK.har", + "@cashier_alipay/cashiersdk": "^15.8.43", + "@uni_modules/uni-payment-alipay": "^1.0.2" + }, + "devDependencies": { + "@ohos/hypium": "1.0.25", + "@ohos/hamock": "1.0.0" + }, + "dynamicDependencies": {} +} \ No newline at end of file diff --git a/products/app/.gitignore b/products/app/.gitignore new file mode 100644 index 0000000..e2713a2 --- /dev/null +++ b/products/app/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/products/app/build-profile.json5 b/products/app/build-profile.json5 new file mode 100644 index 0000000..bc0010f --- /dev/null +++ b/products/app/build-profile.json5 @@ -0,0 +1,36 @@ +{ + "apiType": "stageMode", + "buildOption": { + "resOptions": { + "copyCodeResource": { + "enable": false + } + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default", + "output": { + "artifactName": "rabbit_harmony_2657954" + } + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/products/app/hvigorfile.ts b/products/app/hvigorfile.ts new file mode 100644 index 0000000..53aeaba --- /dev/null +++ b/products/app/hvigorfile.ts @@ -0,0 +1,27 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; +import fs from 'fs'; +import path from 'path'; + +function updateArtifactName() { + const filePath = path.resolve(__dirname, 'build-profile.json5'); + if (fs.existsSync(filePath)) { + const now = new Date(); + const timeStr = `${now.getFullYear().toString().slice(-2)}${now.getMonth() + 1}${now.getDate()}${now.getHours()}${now.getMinutes()}`; + const newArtifactName = `rabbit_harmony_${timeStr}`; + + let content = fs.readFileSync(filePath, 'utf8'); + // 使用正则表达式精准匹配 BUILD_TIME 后的值并替换,匹配模式: "BUILD_TIME": "任意内容" + const newContent = content.replace(/("artifactName"\s*:\s*")[^"]*(")/g, `$1${newArtifactName}$2`); + + if (content !== newContent) { + fs.writeFileSync(filePath, newContent, 'utf8'); + } + + } +} +updateArtifactName(); + +export default { + system: hapTasks, + plugins: [] +} diff --git a/products/app/obfuscation-rules.txt b/products/app/obfuscation-rules.txt new file mode 100644 index 0000000..1e7e54e --- /dev/null +++ b/products/app/obfuscation-rules.txt @@ -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 \ No newline at end of file diff --git a/products/app/oh-package-lock.json5 b/products/app/oh-package-lock.json5 new file mode 100644 index 0000000..d1207de --- /dev/null +++ b/products/app/oh-package-lock.json5 @@ -0,0 +1,29 @@ +{ + "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", + "main_layout@../../features/mainLayout": "main_layout@../../features/mainLayout" + }, + "packages": { + "@ohos/common@../../common": { + "name": "@ohos/common", + "version": "0.0.1", + "resolved": "../../common", + "registryType": "local" + }, + "main_layout@../../features/mainLayout": { + "name": "main_layout", + "version": "1.0.0", + "resolved": "../../features/mainLayout", + "registryType": "local", + "dependencies": { + "@ohos/common": "file:../../common" + } + } + } +} \ No newline at end of file diff --git a/products/app/oh-package.json5 b/products/app/oh-package.json5 new file mode 100644 index 0000000..1fa23f3 --- /dev/null +++ b/products/app/oh-package.json5 @@ -0,0 +1,12 @@ +{ + "name": "app", + "version": "0.0.1", + "description": "项目主模块,入口、启动页、登录、导航栏,首页和我的页面等", + "main": "", + "author": "Elemt", + "license": "", + "dependencies": { + "main_layout": "file:../../features/mainLayout", + "@ohos/common": "file:../../common" + } +} \ No newline at end of file diff --git a/products/app/src/main/ets/appability/AppAbility.ets b/products/app/src/main/ets/appability/AppAbility.ets new file mode 100644 index 0000000..a2d198a --- /dev/null +++ b/products/app/src/main/ets/appability/AppAbility.ets @@ -0,0 +1,143 @@ +import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { KVStore } from '@ohos/common/src/main/ets/utils/KVStore'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import { AppUtil } from '@pura/harmony-utils'; +import { init } from '@dcloudio/uni-app-runtime'; +import * as wxopensdk from '@tencent/wechat_open_sdk'; +import { emitter } from '@kit.BasicServicesKit'; +import { + EVT_BIND_WX_AUTHOR_SUCCESS, + EVT_LOGIN_WX_AUTHOR_SUCCESS } from '@ohos/common/src/main/ets/constants/EventConstants'; +import { AFServiceCenter } from '@alipay/afservicesdk'; +import { WXApi } from '@ohos/common/src/main/ets/viewmodel/CommonService'; +import { setWechatAuthCode } from '@ohos/common/src/main/ets/utils/KVManager'; +import { Logger } from '@ohos/common'; +import { WX_BIND_AUTHOR } from '@ohos/common/src/main/ets/constants/AppConstants'; + + +const DOMAIN = 0x0000; + +export default class AppAbility extends UIAbility { + + onCreate(want: Want, _launchParam: AbilityConstant.LaunchParam): void { + try { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + AppUtil.init(this.context); + KVStore.init(this.context); + this.handleWxResponse(want) + } catch (err) { + hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err)); + } + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onNewWant(want: Want, _launchParam: AbilityConstant.LaunchParam): void { + // 所有回调(如:微信、支付宝等授权完成)都在这里统一处理:支付宝-com.alipay.mobile.client,微信- + const callerBundle = want.parameters?.['ohos.aafwk.param.callerBundleName']; + if(callerBundle === 'com.alipay.mobile.client'){ + //支付宝 + AFServiceCenter.handleResponse(want); + }else{ + //微信 + this.handleWxResponse(want) + } + + } + + private handleWxResponse(want: Want) { + WXApi.handleWant(want, { + onResp(resp: wxopensdk.BaseResp) { + // 处理微信回传响应(即授权结果) + if (resp instanceof wxopensdk.SendAuthResp) { + if (resp.errCode === 0) { + Logger.info('LoginWechat', '微信授权回调----->微信授权成功,授权码: ' + resp.code); + const authCode = resp.code; + // 保存授权码,用于后续使用 + setWechatAuthCode(authCode) + const eventData: emitter.EventData = { + data: { "result": authCode } + }; + const bindAuthorType = AppStorage.get(WX_BIND_AUTHOR) ?? 0; + if(bindAuthorType === 1){ + emitter.emit({ eventId: EVT_LOGIN_WX_AUTHOR_SUCCESS }, eventData); + }else if(bindAuthorType === 2){ + emitter.emit({ eventId: EVT_BIND_WX_AUTHOR_SUCCESS }, eventData); + } + } else { + Logger.info('LoginWechat', '微信授权失败,错误码: ' + resp.errCode); + } + AppStorage.setOrCreate(WX_BIND_AUTHOR, 0); + } + }, + + onReq(req: wxopensdk.BaseReq) { + // 授权登录场景下,微信通常不会主动向 App 发送 Req,留空即可 + Logger.info('LoginWechat', '收到微信请求: ' + JSON.stringify(req)); + } + } as wxopensdk.WXApiEventHandler); + } + + + onDestroy(): void { + // 生命周期管理:销毁 + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // 生命周期管理:主窗口已创建,为此功能设置主页 + //获取主窗口,设置沉浸式 + windowStage.getMainWindow((err, data) => { + let windowClass: window.Window = data; + // 1. 设置窗口为全屏模式(沉浸式) + windowClass.setWindowLayoutFullScreen(true); + + // 2. 设置状态栏文字颜色(白色/黑色) + let sysBarProps: window.SystemBarProperties = { + statusBarContentColor: '#000000' // 黑色文字 + }; + windowClass.setSystemBarProperties(sysBarProps); + + // 3. 获取避让区域(状态栏等) + // 注意:使用 window.AvoidAreaType.TYPE_SYSTEM 明确类型 + let type: window.AvoidAreaType = window.AvoidAreaType.TYPE_SYSTEM; + let avoidArea: window.AvoidArea = windowClass.getWindowAvoidArea(type); + + // 4. 计算高度并存储 (px2vp 是内置函数,直接使用) + let statusBarHeight: number = px2vp(avoidArea.topRect.height); + + // 5. 存入全局存储,Key 名为 'statusBarHeight' + AppStorage.setOrCreate('statusBarHeight', statusBarHeight); + }); + + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + init(this, windowStage, { + debug: true + }) + + + // 加载页面(启动页) + windowStage.loadContent('pages/SplashScreenPage', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // 生命周期管理:主窗口已销毁,释放与UI相关的资源 + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // 生命周期管理:已处于前台页面 + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // 生命周期管理:已处于后台页面 + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ No newline at end of file diff --git a/products/app/src/main/ets/appbackupability/AppBackupAbility.ets b/products/app/src/main/ets/appbackupability/AppBackupAbility.ets new file mode 100644 index 0000000..4118ec6 --- /dev/null +++ b/products/app/src/main/ets/appbackupability/AppBackupAbility.ets @@ -0,0 +1,16 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class AppBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/products/app/src/main/ets/constants/CommonConstants.ets b/products/app/src/main/ets/constants/CommonConstants.ets new file mode 100644 index 0000000..050bbb5 --- /dev/null +++ b/products/app/src/main/ets/constants/CommonConstants.ets @@ -0,0 +1,181 @@ +interface CommonConstantsInterface { + columnXs: number; + columnSm: number; + columnMd: number; + columnLg: number; + spanXs: number; + spanSm: number; + spanMd: number; + spanLg: number; + offsetXs: number; + offsetSm: number; + offsetMd: number; + offsetLg: number; + listFontWeight: number; + listWidth: string; + listItemWidth: string; + columnHeight: string; + bundleName: string; + abilityName: string; + breakpointsInitializeName: string; + locationCapability: string; + breakpointSystemName: string; + systemColumnHeight: string; + systemColumnWidth: string; + systemRowWidth: string; + textFontWeight: number; + adaptiveColumnWidth: string; + adaptiveColumnHeight: string; + adaptiveFlexibleColumnWidth: string; + adaptiveFlexShrink: number; +} + +/** + * Common constants for all features. + */ +export const commonConstants: CommonConstantsInterface = { + /** + * The number of columns for XS device. + */ + columnXs: 4, + + /** + * The number of columns for SM device. + */ + columnSm: 4, + + /** + * The number of columns for MD device. + */ + columnMd: 8, + + /** + * The number of columns for LG device. + */ + columnLg: 12, + + /** + * The number of span for XS device. + */ + spanXs: 4, + + /** + * The number of span for SM device. + */ + spanSm: 4, + + /** + * The number of span for MD device. + */ + spanMd: 4, + + /** + * The number of span for LG device. + */ + spanLg: 4, + + /** + * The number of offset for XS device. + */ + offsetXs: 0, + + /** + * The number of offset for SM device. + */ + offsetSm: 0, + + /** + * The number of offset for MD device. + */ + offsetMd: 2, + + /** + * The number of offset for LG device. + */ + offsetLg: 4, + + /** + * Font weight of the list. + */ + listFontWeight: 500, + + /** + *Width of the list. + */ + listWidth: '100%', + + /** + * The percentage of width of the list item. + */ + listItemWidth: '100%', + + /** + * The percentage of height of the column. + */ + columnHeight: '100%', + + /** + * The name of the bundleName. + */ + bundleName: 'com.img.rabbit', + + /** + * The name of the abilityName. + */ + abilityName: 'ResponsiveLayoutItemAbility', + + /** + * Initialize device breakpoints. + */ + breakpointsInitializeName: 'md', + + /** + * Localization ability. + */ + locationCapability: 'SystemCapability.Location.Location.Core', + + /** + * Breakpoint name. + */ + breakpointSystemName: 'mainBreakpoint', + + /** + * The percentage of height of column component. + */ + systemColumnHeight: '100%', + + /** + * The percentage of width of column component. + */ + systemColumnWidth: '100%', + + /** + * The percentage of width of row component. + */ + systemRowWidth: '100%', + + /** + * Font weight of text component. + */ + textFontWeight: 500, + + /** + * The percentage of width of column component. + */ + adaptiveColumnWidth: '100%', + + /** + * The percentage of height of column component. + */ + adaptiveColumnHeight: '100%', + + /** + * The percentage of height of column component. + */ + adaptiveFlexibleColumnWidth: '100%', + + /** + * The value of shrink of column component. + */ + adaptiveFlexShrink: 1 +}; \ No newline at end of file diff --git a/products/app/src/main/ets/pages/Index.ets b/products/app/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..f4512a4 --- /dev/null +++ b/products/app/src/main/ets/pages/Index.ets @@ -0,0 +1,61 @@ +import { NavigationPage } from './NavigationPage'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; + +@Builder +export function IndexBuilder() { + Index() +} + +/** + * 入口程序 + */ +@Entry +@Component +struct Index { + //兼容方式 + //private context = getContext(this) as common.UIAbilityContext; + //高版本(SDK>=12)使用以下方式获取 + //private context = this.getUIContext()?.getHostContext() as common.UIAbilityContext | undefined; + // private windowClass: window.Window | undefined = undefined; + private lastExitTime: number = 0; + + /* + aboutToAppear() { + // 获取窗口实例并设置沉浸式 + window.getLastWindow(this.context).then((data: window.Window) => { + this.windowClass = data; + // 1. 设置全屏布局 + this.windowClass.setWindowLayoutFullScreen(true).catch(() => { + + }).then(() => { + // 2. 隐藏状态栏和导航栏 + this.windowClass?.setWindowSystemBarEnable([]).catch(() => { + }); + }); + }); + } + + aboutToDisappear() { + // 页面退出时恢复系统栏 + this.windowClass?.setWindowSystemBarEnable(['status', 'navigation']).catch(() => { + }); + } + */ + + onBackPress() { + let currentTime = new Date().getTime(); + if (currentTime - this.lastExitTime > 2000) { + this.lastExitTime = currentTime; + ToastUtils.normalToast({message: '再按一次退出应用'}); + + return true; + } + return false; + } + + build() { + Column() { + NavigationPage(); + } + } +} \ No newline at end of file diff --git a/products/app/src/main/ets/pages/LoginPage.ets b/products/app/src/main/ets/pages/LoginPage.ets new file mode 100644 index 0000000..b0fb953 --- /dev/null +++ b/products/app/src/main/ets/pages/LoginPage.ets @@ -0,0 +1,781 @@ +import router from '@ohos.router'; +import { WebParams } from '@ohos/common/src/main/ets/viewmodel/ParamBean'; +import { TitleBarComponent } from 'main_layout/src/main/ets/view/TitleBarComponent'; +import { AGREEMENT_URL, IS_AGREE_AGREEMENT, PRIVACY_URL, + WX_BIND_AUTHOR } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { KVStore } from '@ohos/common/src/main/ets/utils/KVStore'; +import { ToastUtils } from '@ohos/common/src/main/ets/dialog/ToastUtils'; +import { routerIndex } from '@ohos/common/src/main/ets/router/RouterManager'; +import { CommonService } from '@ohos/common/src/main/ets/viewmodel/CommonService'; +import { common } from '@kit.AbilityKit'; +import { emitter } from '@kit.BasicServicesKit'; +import { EVT_LOGIN_WX_AUTHOR_SUCCESS } from '@ohos/common/src/main/ets/constants/EventConstants'; +import { Logger } from '@ohos/common'; + +/** + * 登录页面 + */ +@Entry +@Component +struct LoginPage { + @Provide('pageInfos') pageStack: NavPathStack = new NavPathStack(); + @State fromSource: string = ''; + @State hasAgreed?: boolean = undefined; + @State loginMethod?: number = 0; //登录方式:0 手机,1 一键登录,2 微信,3 支付宝 + + /* + dialogController: CustomDialogController = new CustomDialogController({ + builder: AgreementDialog({ + confirm: () => { + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + } + }), + // 1. 禁止点击空白处关闭 + autoCancel: false, + + // 2. 拦截返回手势 (API 11+),如果用户触发返回手势,此回调会被触发,不调用 dismiss() 则弹窗不会消失。 + onWillDismiss: (dismissDialogAction: DismissDialogAction) => { + console.info("Reason: " + JSON.stringify(dismissDialogAction.reason)); + // 如果是因为点击遮罩(0)或按下返回键(1)触发,不做处理,从而实现禁止关闭 + if (dismissDialogAction.reason === DismissReason.PRESS_BACK || + dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) { + // 只要不执行 dismissDialogAction.dismiss(),弹窗就永远不消失 + console.info("不允许通过此方式关闭"); + } + }, + alignment: DialogAlignment.Center, + customStyle: true + }) + */ + + + async aboutToAppear() { + // 1. 获取所有参数对象 + const params = router.getParams() as Record; + // 2. 检查并读取具体的 key + if (params && params.from) { + this.fromSource = params.from; + } + + setTimeout(() => { + if(this.fromSource === 'logout' || this.fromSource === 'deleteAccount') { + if (this.pageStack) { + this.pageStack.clear(); + } + router.clear() + } + }, 0); + + emitter.off(EVT_LOGIN_WX_AUTHOR_SUCCESS); + emitter.once({ eventId: EVT_LOGIN_WX_AUTHOR_SUCCESS }, (eventData: emitter.EventData) => { + const payload = eventData.data; + Logger.info('LoginPage', 'wxLogin: ' + JSON.stringify(payload)); + if (payload && payload["result"]) { + const authCode: string = payload["result"] as string; + + // 获取授权码,请求登录 + CommonService.wxLogin(authCode, { + onSuccess: (_) => { + LoginSuccess(this.fromSource) + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '登录失败'); + } + }) + } + }); + + + /* + // 2. 将存储读取逻辑移到这里(生命周期方法支持异步逻辑) + KVStore.getInstance().get(IS_AGREE_AGREEMENT, '').then((val) => { + if(val === '') { + this.hasAgreed = undefined; + }else if(val === 'true'){ + this.hasAgreed = true; + }else { + this.hasAgreed = false; + } + if (!this.hasAgreed) { + this.dialogController.open(); + } + }); + */ + } + + build() { + Navigation(this.pageStack) { + Stack() { + // 1. 背景层 + Stack(){ + Image($r('app.media.ic_login_mask')).width('100%') + } + .width('100%') + .height('100%') + .alignContent(Alignment.TopStart) + + // 2. 主体内容层 (标题 + 登录框) + Column() { + TitleBarComponent({ + titleName: '', + isShowTitle: false, + isShowBack: this.fromSource !== 'splash' && this.fromSource !== 'logout' && this.fromSource !== 'deleteAccount' + }) + + //显示登录内容 + if(this.loginMethod === 0){ + // 手机号登录 + LoginForPhone({isChecked: this.hasAgreed, fromSource: this.fromSource}) + }else if(this.loginMethod === 2){ + // 微信登录 + LoginForWechat({isChecked: this.hasAgreed, fromSource: this.fromSource}) + }else if(this.loginMethod === 3){ + // 支付宝登录 + LoginForAlipay({isChecked: this.hasAgreed, fromSource: this.fromSource}) + }else if(this.loginMethod === 1){ + // 一键登录 + } + } + .width('100%') + .height('100%') + + // 3. 底部“其他方式”层 + Column() { + Row() { + Image($r('app.media.ic_login_other_left_line')) + .margin({ right: 12 }) + .width(60) + .height(2) + + Text('其他方式登录') + .fontSize(16) + .fontColor('#FF767676') + .fontWeight(FontWeight.Bold) + + Image($r('app.media.ic_login_other_right_line')) + .margin({ left: 12 }) + .width(60) + .height(2) + } + .alignItems(VerticalAlign.Center) // 确保横线和文字垂直居中对齐 + + Row(){ + Image($r('app.media.ic_login_wx_icon')) + .width(40) + .height(40) + .onClick(() => { + this.loginMethod = 2; + }) + + Image($r('app.media.ic_login_alipay_icon')) + .width(40) + .height(40) + .margin({ left: 40, right: 40 }) + .onClick(() => { + this.loginMethod = 3; + }) + + Image($r('app.media.ic_login_phone_icon')) + .width(40) + .height(40) + .onClick(() => { + this.loginMethod = 0; + }) + } + .margin({ top: 27, bottom: 60 }) + } + .width('100%') + .margin({ bottom: 40 }) // 距离屏幕底部留一点间距,避免贴边 + } + .alignContent(Alignment.Bottom) // 将 Stack 默认对齐方式设为底部,或者手动控制层级 + .width('100%') + .height('100%') + .backgroundColor($r('sys.color.white')) + } + .hideTitleBar(true) + .mode(NavigationMode.Stack) + } +} + +/** + * 登录-手机验证码(Component) + */ +@Component +struct LoginForPhone { + @Consume('pageInfos') pageStack: NavPathStack; + @State textPhoneValue: string = ''; + @State textCaptchaValue: string = ''; + @State captchaTimestampValue: string = ''; + + @State countdown: number = 0; // 倒计时秒数 + @State isCounting: boolean = false; // 是否正在倒计时 + private timerId: number = -1; // 定时器ID + + @Prop isChecked: boolean = false; + @Prop fromSource: string = ''; + onChange: (checked: boolean) => void = (checked: boolean) => { + if(checked){ 'true' }else{ 'false' } + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + }; + + cancel: () => void = () => {}; + confirm: () => void = () => { + if(!this.textPhoneValue){ + ToastUtils.showToast(this.getUIContext(), '请输入手机号码') + }else if(!this.isChecked){ + ToastUtils.showToast(this.getUIContext(), '请先同意隐私和协议...') + }else if(!this.textCaptchaValue){ + ToastUtils.showToast(this.getUIContext(), '请输入验证码') + }else{ + CommonService.loginForCaptcha(this.textPhoneValue, this.textCaptchaValue, this.captchaTimestampValue, { + onSuccess: (_) => { + LoginSuccess(this.fromSource) + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '登录失败'); + } + }) + } + }; + + + build() { + Column() { + // Logo图标区域 + Row() { + Image($r('app.media.ic_login_vertical_line')) + .width('4vp') + .height('55vp') + + Column(){ + Text('欢迎登录') + .width('100%') + .fontSize(18) + .fontColor('#FF000000') + .textAlign(TextAlign.Start) + .fontWeight(FontWeight.Bold) + + Text('截图兔') + .width('100%') + .fontSize(32) + .fontColor('#FF000000') + .textAlign(TextAlign.Start) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .margin({left: 12}) + + } + .width('100%') + .justifyContent(FlexAlign.Start) + .margin({top: 63}) + + //账号输入框 + Stack({ alignContent: Alignment.BottomEnd }) { + Row(){ + Text('+86') + .fontSize(14) + .fontColor('#FF3D3D3D') + .textAlign(TextAlign.Start) + + Image($r('app.media.ic_login_input_div_line')) + .width(1) + .height(28) + .margin({ left: 12 }) + + TextInput({ placeholder: '请输入手机号', text: this.textPhoneValue }) + .width('100%') + .type(InputType.PhoneNumber) + .maxLength(11) + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .placeholderColor('#FF767676') + .backgroundColor('#00000000') + .borderRadius(8) + .borderColor('#00000000') + .borderWidth(0.5) + .onChange((v) => this.textPhoneValue = v) + } + } + .width('100%') + .backgroundColor('#FFF6F6F6') + .borderRadius(8) + .borderColor('#FFDEDEDE') + .borderWidth(0.5) + .margin({top: 100}) + .padding({ left: 16, right: 16}) + + //验证码输入框 + Stack({ alignContent: Alignment.BottomEnd }) { + Row(){ + TextInput({ placeholder: '请输入验证码', text: this.textCaptchaValue }) + .layoutWeight(1) + .type(InputType.Number) + .maxLength(20) + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .placeholderColor('#FF767676') + .backgroundColor('#00000000') + .borderRadius(8) + .borderColor('#00000000') + .borderWidth(0.5) + .onChange((v) => this.textCaptchaValue = v) + Stack() { + Text(this.isCounting ? `${this.countdown}s后重新获取` : '获取验证码') + .fontSize(12) + .fontColor(this.isCounting ? '#FF999999' : '#FF3D3D3D') // 倒计时变灰 + .textAlign(TextAlign.Start) + } + .backgroundColor('#FFFFFFFF') + .borderRadius(8) + .borderColor('#00000000') + .borderWidth(0.5) + .padding({ left: 19, right: 19, top: 12, bottom: 12}) + .onClick(() => { + if (this.isCounting) return; // 倒计时中点击无效 + + if (!this.textPhoneValue || this.textPhoneValue.length < 11) { + ToastUtils.showToast(this.getUIContext(), '请输入正确的手机号码'); + return; + } + + CommonService.sendSmsCode(this.textPhoneValue,{ + onSuccess: (captchaTimestamp) => { + this.captchaTimestampValue = captchaTimestamp.toString() + // 发送成功后调用倒计时 + this.startCountdown(); + + ToastUtils.normalToast({ + message: '验证码已发送', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '发送失败'); + } + }) + + }) + + } + } + .width('100%') + .backgroundColor('#FFF6F6F6') + .borderRadius(12) + .borderColor('#FFDEDEDE') + .borderWidth(0.5) + .margin({top: 20}) + .padding({ left: 14, right: 5}) + + //登录按钮 + Button('登录') + .onClick(() => { + this.confirm(); + }) + .backgroundColor(Color.Black) // 对应图片中的黑色按钮 + .fontColor('#FFC2FF43') + .borderRadius(359) + .fontSize(16) + .height(46) + .width('100%') + .margin({top: 46, bottom:14}) + + //用户协议 + Row(){ + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(16) + .height(16) + .margin({ right: 4 }) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + + Text() { + Span('我已阅读并同意') + .fontColor('#80aaaaaa') + + Span('《用户协议》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: AGREEMENT_URL + }; + this.pageStack.pushPathByName("WebLayout", params) + }) + + Span('和') + .fontColor('#80aaaaaa') + + Span('《隐私政策》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: PRIVACY_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + } + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ right: 4 }) + + } + } + .width('100%') + .height('100%') + .padding({ left: 38, right: 38}) + } + + // 开启倒计时 + startCountdown() { + if (this.isCounting) return; + + this.countdown = 60; + this.isCounting = true; + + // 清理可能存在的旧定时器 + clearInterval(this.timerId); + + // 使用箭头函数确保 this 指向 LoginForPhone 组件 + this.timerId = setInterval(() => { + if (this.countdown > 1) { + this.countdown--; + } else { + this.stopCountdown(); + } + }, 1000); + } + + + // 停止倒计时 + stopCountdown() { + this.isCounting = false; + this.countdown = 0; + clearInterval(this.timerId); + } + + // 组件销毁前清理,防止内存泄漏 + aboutToDisappear() { + this.stopCountdown(); + } +} + +/** + * 登录-微信(Component) + */ +@Component +struct LoginForWechat { + @Consume('pageInfos') pageStack: NavPathStack; + + @Prop fromSource: string = ''; + @Prop isChecked: boolean = false; + onChange: (checked: boolean) => void = (checked: boolean) => { + if(checked){ 'true' }else{ 'false' } + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + }; + build() { + Column(){ + Image($r('app.media.ic_app_logo')) + .width('100vp') + .height('100vp') + .margin({ top: 120 }) + .align(Alignment.Center) + + Text('截图兔') + .fontSize(16) + .fontColor('#FF000000') + .textAlign(TextAlign.Center) + .fontWeight(FontWeight.Bold) + .margin({ top: 8 }) + + Column() { + Text('为了更好地为您提供服务,请先完成微信授权') + .fontSize(14) + .fontColor('#805d5c5c') + .textAlign(TextAlign.Center) + .margin({ top: 30 }) + + Row() { + Image($r('app.media.ic_login_wx_auth_icon')) + .width(16) + .height(16) + .margin({ right: 4 }) + + Text('微信授权登录') + .fontSize(16) + .fontColor('#FFC2FF43') + .textAlign(TextAlign.Center) + } + .width('100%') + .height(48) + .backgroundColor('#FF252525') + .borderRadius(359) + .margin({ top: 8 }) + .padding({ top: 6, bottom: 6 }) + .align(Alignment.Center) + .justifyContent(FlexAlign.Center) + .onClick(() => { + if (this.isChecked) { + AppStorage.setOrCreate(WX_BIND_AUTHOR, 1); + CommonService.wxAuthorize(getContext(this) as common.UIAbilityContext, false); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先同意隐私和协议...') + } + }) + } + .margin({ bottom:14 }) + + //用户协议 + Row(){ + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(16) + .height(16) + .margin({ right: 4 }) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + + Text() { + Span('我已阅读并同意') + .fontColor('#80aaaaaa') + + Span('《用户协议》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: AGREEMENT_URL + }; + this.pageStack.pushPathByName("WebLayout", params) + }) + + Span('和') + .fontColor('#80aaaaaa') + + Span('《隐私政策》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: PRIVACY_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + } + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ right: 4 }) + + } + } + .width('100%') + .height('100%') + .padding({ left: 16, right: 16}) + } +} + +/** + * 登录-支付宝(Component) + */ +@Component +struct LoginForAlipay { + @Consume('pageInfos') pageStack: NavPathStack; + @Prop fromSource: string = ''; + @Prop isChecked: boolean = false; + onChange: (checked: boolean) => void = (checked: boolean) => { + if(checked){ 'true' }else{ 'false' } + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true'); + }; + build() { + Column(){ + Image($r('app.media.ic_app_logo')) + .width('100vp') + .height('100vp') + .margin({ top: 120 }) + .align(Alignment.Center) + + Text('截图兔') + .fontSize(16) + .fontColor('#FF000000') + .textAlign(TextAlign.Center) + .fontWeight(FontWeight.Bold) + .margin({ top: 8 }) + + Column() { + Text('为了更好地为您提供服务,请先完成支付宝授权') + .fontSize(14) + .fontColor('#805d5c5c') + .textAlign(TextAlign.Center) + .margin({ top: 30 }) + + Row() { + Image($r('app.media.ic_login_alipay_auth_icon')) + .width(16) + .height(16) + .margin({ right: 4 }) + + Text('支付宝授权登录') + .fontSize(16) + .fontColor('#FFC2FF43') + .textAlign(TextAlign.Center) + } + .width('100%') + .height(48) + .backgroundColor('#FF252525') + .borderRadius(359) + .margin({ top: 8 }) + .padding({ top: 6, bottom: 6 }) + .align(Alignment.Center) + .justifyContent(FlexAlign.Center) + .onClick(() => { + if(this.isChecked){ + CommonService.getAlipayLoginParams().then(res => { + CommonService.aliAuthorize(res, (result: string) => { + //阿里支付宝授权成功后返回openId用于登录使用 + CommonService.aliLogin(result, { + onSuccess: (_) => { + LoginSuccess(this.fromSource) + }, + onFailure: (message) => { + ToastUtils.showToast(this.getUIContext(), message); + }, + onError: (_) => { + ToastUtils.showToast(this.getUIContext(), '登录失败'); + } + }); + }); + }); + }else{ + ToastUtils.showToast(this.getUIContext(), '请先同意隐私和协议...') + } + }) + } + .margin({ bottom:14 }) + + //用户协议 + Row(){ + Image(this.isChecked ? + $r('app.media.ic_tick_circle_selected') : // 选中的图片资源 + $r('app.media.ic_tick_circle_normal') // 未选中的图片资源 + ) + .width(16) + .height(16) + .margin({ right: 4 }) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.isChecked = !this.isChecked; + this.onChange(this.isChecked); + }) + // 可选:添加点击时的缩放动画,增加反馈感 + .animation({ duration: 200, curve: Curve.EaseInOut }) + + Text() { + Span('我已阅读并同意') + .fontColor('#80aaaaaa') + + Span('《用户协议》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: AGREEMENT_URL + }; + this.pageStack.pushPathByName("WebLayout", params) + }) + + Span('和') + .fontColor('#80aaaaaa') + + Span('《隐私政策》') + .fontColor('#FFAAAAAA') // 高亮颜色(深灰) + .fontWeight(FontWeight.Bold) + .onClick(() => { + const params: WebParams = { + id: 1001, + name: "用户协议", + type: 1, + url: PRIVACY_URL + }; + this.pageStack.pushPathByName("WebLayout", params); + }) + } + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ right: 4 }) + + } + } + .width('100%') + .height('100%') + .padding({ left: 16, right: 16}) + } +} + +/** + * 登录成功 (逻辑处理) + * @param source + */ +function LoginSuccess(source: string) { + ToastUtils.normalToast({ + message: '登录成功', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + + //重新获取用户配置(数据会持久化到本地) + CommonService.getUserProfile().then(_ => { + // 当用户配置获取完成后,重新获取用户信息(数据会持久化到本地) + CommonService.getUserInfo() + }) + + if(source === 'splash' || source === 'logout' || source === 'deleteAccount'){ + //登录成功后回到首页 + routerIndex() + }else{ + AppStorage.setOrCreate('back', { isBack: true, source: 'login' }); + router.back(); + } +} \ No newline at end of file diff --git a/products/app/src/main/ets/pages/NavigationPage.ets b/products/app/src/main/ets/pages/NavigationPage.ets new file mode 100644 index 0000000..1f69fb9 --- /dev/null +++ b/products/app/src/main/ets/pages/NavigationPage.ets @@ -0,0 +1,125 @@ +import { BreakpointSystem, BreakPointType } from '@ohos/common'; +import { commonConstants } from 'main_layout/src/main/ets/constants/CommonConstants'; +import TabsViewModel from '../viewmodel/TabsViewModel'; +import { HomeComponent } from '../view/HomeComponent'; +import { MineComponent } from '../view/MineComponent'; +import { TabBarItem } from '../viewmodel/LocalBean'; + +/** + * 入口布局 + */ +@Builder +export function NavigationPage() { + Column() { + TabIndex() + } +} + +@Component +export struct TabIndex { + @State currentIndex: number = 0; + @State tabs: TabBarItem[] = TabsViewModel.getTabData(); + @StorageLink('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName; + private readonly breakpointSystem: BreakpointSystem = new BreakpointSystem(commonConstants.breakpointSystemName); + @Provide('pageInfos') pageStack: NavPathStack = new NavPathStack(); + + @Builder tabBarBuilder(tabBar: TabBarItem) { + Flex({ + direction: new BreakPointType({ + sm: FlexDirection.Column, + md: FlexDirection.Row, + lg: FlexDirection.Column, + xl: FlexDirection.Column + }) + .getValue(this.currentBreakpoint), justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center + }) { + Image(this.currentIndex === tabBar.id ? tabBar.selectIcon:tabBar.icon) + .width(this.currentBreakpoint === 'sm' ? 18 : 18) + .height(this.currentBreakpoint === 'sm' ? 18 : 18) + + Text(tabBar.name) + .fontSize($r('app.float.tab_text_font_size')) + .fontColor(this.currentIndex === tabBar.id ? $r('app.color.tab_font_color_selected') : $r('app.color.tab_font_color_normal')) + .margin(new BreakPointType({ + sm: { top: $r('app.float.tab_text_sm_top'), left: $r('app.float.tab_text_sm_left') }, + md: { top: $r('app.float.tab_text_md_top'), left: $r('app.float.tab_text_md_left') }, + lg: { top: $r('app.float.tab_text_lg_top'), left: $r('app.float.tab_text_lg_left') }, + xl: { top: $r('app.float.tab_text_xl_top'), left: $r('app.float.tab_text_xl_left') } + }) + .getValue(this.currentBreakpoint)) + } + .width(commonConstants.flexTabBarWidth) + .height(commonConstants.flexTabBarHeight) + } + + aboutToAppear() { + this.breakpointSystem.register(this.getUIContext()); + } + + aboutToDisappear() { + this.breakpointSystem.unregister(); + } + + build() { + Navigation(this.pageStack) { + Column() { + Tabs({ + barPosition: new BreakPointType({ + sm: BarPosition.End, + md: BarPosition.End, + lg: BarPosition.Start, + xl: BarPosition.Start + }) + .getValue(this.currentBreakpoint) + }) { + ForEach(this.tabs, (item: TabBarItem) => { + TabContent() { + if (item.id === 0) { + HomeComponent() + } else { + MineComponent() + } + } + .tabBar(this.tabBarBuilder(item)) + .width(commonConstants.tabContentWidth) + .height(commonConstants.tabContentHeight) + }, (item: TabBarItem) => item.id.toString()) + } + .vertical(new BreakPointType({ + sm: commonConstants.tabSmVertical, + md: commonConstants.tabMdVertical, + lg: commonConstants.tabLgVertical, + xl: commonConstants.tabXlVertical + }) + .getValue(this.currentBreakpoint)) + .barWidth(new BreakPointType({ + sm: commonConstants.tabSmBarWidth, + md: commonConstants.tabMdBarWidth, + lg: commonConstants.tabLgBarWidth, + xl: commonConstants.tabXlBarWidth + }) + .getValue(this.currentBreakpoint)) + .barHeight(new BreakPointType({ + sm: commonConstants.tabSmBarHeight, + md: commonConstants.tabMdBarHeight, + lg: commonConstants.tabLgBarHeight, + xl: commonConstants.tabXlBarHeight + }) + .getValue(this.currentBreakpoint)) + .onAnimationStart((_: number, targetIndex: number) => { + this.currentIndex = targetIndex; + }) + .divider({ + strokeWidth: 0.5, + color: $r('app.color.tab_line'), + startMargin: 0, + endMargin: 0 + }) + } + .backgroundColor($r('app.color.normal_background')) + } + .hideTitleBar(true) + .mode(NavigationMode.Stack) + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } +} \ No newline at end of file diff --git a/products/app/src/main/ets/pages/SplashScreenPage.ets b/products/app/src/main/ets/pages/SplashScreenPage.ets new file mode 100644 index 0000000..c706379 --- /dev/null +++ b/products/app/src/main/ets/pages/SplashScreenPage.ets @@ -0,0 +1,223 @@ +import { Logger } from '@ohos/common'; +import { KVStore } from '@ohos/common/src/main/ets/utils/KVStore'; +import { + DEVICE_OAID, + IS_AGREE_AGREEMENT, + IS_KV_STORE_INIT_FINISHED, + TOKEN, + USER_INFO, + USER_PROFILE } from '@ohos/common/src/main/ets/constants/AppConstants'; +import { AgreementDialog } from '@ohos/common/src/main/ets/dialog/AgreementDialog'; +import emitter from '@ohos.events.emitter'; +import { EVT_KV_INIT_DONE } from '@ohos/common/src/main/ets/constants/EventConstants'; +import { UserService } from '../viewmodel/UserService'; +import { getOAID } from '@ohos/common/src/main/ets/utils/AppUtils'; +import { routerIndex, routerLogin } from '@ohos/common/src/main/ets/router/RouterManager'; +import { CommonService } from '@ohos/common/src/main/ets/viewmodel/CommonService'; +import { UserProfile } from '@ohos/common/src/main/ets/viewmodel/DataBean'; +import { registerUniProvider } from '@dcloudio/uni-app-runtime'; +import { UniPaymentAlipayProviderImpl } from '@uni_modules/uni-payment-alipay'; + +/** + * 启动页 - Splash Screen + * 显示应用Logo和加载动画,1秒后自动跳转到主页面 + * 为了给用户更好的体验,采用“谁快听谁的,但有保底时间”的策略。 + */ +@Entry +@Component +struct SplashScreenPage { + @State countdown: number = 1; // 修改为5秒倒计时 + private timerId: number = -1; + @State canSkip: boolean = false; + @State hasAgreed?: boolean = undefined; + @State isShowCountDown: boolean = false; + + // 用于存储请求到的配置或错误状态 + private isNavigated: boolean = false; + + dialogController: CustomDialogController = new CustomDialogController({ + builder: AgreementDialog({ + confirm: () => { + // --- 场景1:点击同意弹窗后触发 --- + KVStore.getInstance().put(IS_AGREE_AGREEMENT, 'true').then(() => { + this.agreeRun() + }); + } + }), + autoCancel: false, + onWillDismiss: (dismissDialogAction: DismissDialogAction) => { + if (dismissDialogAction.reason === DismissReason.PRESS_BACK || + dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) { + Logger.info('Rabbit_SplashScreenPage', '必须同意协议才能进入'); + } + }, + alignment: DialogAlignment.Center, + customStyle: true + }) + + async loadUserConfig() { + try { + // 1. 获取系统基本信息 (同步),获取服务器时间,并与本地时间同步 + await UserService.getSystemTime() + // 2. 等待时间同步完成后发,起用户配置请求(数据会持久化到本地) + const profile = await CommonService.getUserProfile(); + // 3. 等待用户配置完成后,发起用户信息请求(数据会持久化到本地) + await CommonService.getUserInfo() + Logger.info('Rabbit_SplashScreenPage', `配置获取成功----> token:${profile.token} name:${profile.name}`); + } catch (err) { + Logger.error('Rabbit_SplashScreenPage', '请求失败,请检查参数或者接口返回数据:' + err); + } finally { + // 如果希望请求一回来就跳转(不等待5秒完),立即执行跳转: + // this.navigateToMain(); + } + } + + + aboutToAppear() { + Logger.info('Rabbit_SplashScreenPage', '启动页加载完成'); + getOAID().then((oaid: string) => { + AppStorage.setOrCreate(DEVICE_OAID, oaid); + }); + + if(AppStorage.get(IS_KV_STORE_INIT_FINISHED)){ + this.showAgreement() + }else{ + //等待KVStore初始化完成事件(KVStore初始化完成,执行获取协议,如同意执行跳转,否则弹协议窗) + emitter.once({ eventId: EVT_KV_INIT_DONE }, () => { + this.showAgreement() + }); + } + } + + showAgreement() { + KVStore.getInstance().get(IS_AGREE_AGREEMENT, '').then((val: string) => { + Logger.info('Rabbit_SplashScreenPage', val === 'true'?'已同意协议,计时开始...':'未同意协议,需要弹窗展示协议'); + + if (val === 'true') { + // --- 场景2:已同意过,直接进入 --- + this.agreeRun() + } else { + // 未同意,显示弹窗 + this.hasAgreed = false; + this.dialogController.open(); + } + }); + } + + // 同意协议后执行 + agreeRun() { + this.hasAgreed = true; + this.startFlow(); + } + + /** + * 统一启动流程:延迟显示跳过按钮 + 开启倒计时 + */ + startFlow(): void { + // 延迟500ms后显示跳过按钮 + setTimeout(() => { + this.canSkip = true; + }, 500); + + // 启动倒计时定时器 + this.startCountdown(); + this.loadUserConfig(); + registerUniProvider("payment", "alipay", new UniPaymentAlipayProviderImpl()); + } + + startCountdown(): void { + this.clearTimer(); // 防御性清除,避免重复启动 + this.timerId = setInterval(() => { + this.countdown -= 1; + if (this.countdown <= 0) { + this.navigateToMain(); + } + }, 1000); // 注意:秒级倒计时应使用1000ms,500ms会导致2.5秒就结束 + } + + clearTimer(): void { + if (this.timerId !== -1) { + clearInterval(this.timerId); + this.timerId = -1; + } + } + + navigateToMain(): void { + if (this.isNavigated) return; // 防止重复跳转 + this.isNavigated = true; + this.clearTimer(); + + const profile = AppStorage.get(USER_PROFILE); + KVStore.getInstance().get(TOKEN, '').then((accountToken: string) => { + //无论是游客还是登录用户都应该拥有自己的token,如果没有跳转到登录页处理 + if (profile && (profile.token !== '' || accountToken !== '')) { + // if (profile) { + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + if(userJson === '{}'){ + CommonService.getUserInfo() + } + }) + // 只有在 5 秒内请求成功并拿到了 token 才去首页 + routerIndex() + } else { + // 请求超时、失败、或还没回来的兜底方案:去登录页 + routerLogin('splash',true) + } + }) + } + + aboutToDisappear() { + this.clearTimer(); + } + + // 跳过按钮点击事件 + onSkipClick() { + if (this.canSkip) { + this.navigateToMain(); + } + } + + build() { + Stack() { + // 1. 背景图 + Image($r('app.media.ic_splash_mask')) + .width('100%') + .height('100%') + .objectFit(ImageFit.Cover) + + // 2. 居中的 Logo + Column() { + Image($r('app.media.ic_splash_logo')) + .width('151vp') + .height('58vp') + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + + // 3. 右上角倒计时与跳过按钮 + if (this.hasAgreed === true && this.isShowCountDown) { // 只有同意协议后才显示 + Row() { + Text(`${this.countdown}s | 跳过`) + .fontSize('14fp') + .fontColor(Color.White) + .fontWeight(FontWeight.Medium) + } + .position({ x: '100%', y: 0 }) // 定位到右上角 + .markAnchor({ x: '100%', y: 0 }) // 锚点修正 + .margin({ top: px2vp(AppStorage.get('statusBarHeight') ?? 0) + 20, right: 20 }) // 避让状态栏 + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + .backgroundColor('#40000000') // 40% 不透明度的黑色背景 + .borderRadius(15) // 圆角胶囊形状 + .visibility(this.canSkip ? Visibility.Visible : Visibility.Hidden) // 控制500ms后显示 + .onClick(() => { + this.onSkipClick(); + }) + } + } + .width('100%') + .height('100%') + .backgroundColor('#FFFFFF') + } + +} \ No newline at end of file diff --git a/products/app/src/main/ets/view/HomeComponent.ets b/products/app/src/main/ets/view/HomeComponent.ets new file mode 100644 index 0000000..ab935b7 --- /dev/null +++ b/products/app/src/main/ets/view/HomeComponent.ets @@ -0,0 +1,875 @@ +import { HomeItem } from "../viewmodel/LocalBean"; +import HomeViewModel from "../viewmodel/HomeViewModel"; +import { CuteParams } from "@ohos/common/src/main/ets/viewmodel/ParamBean"; +import { ToastUtils } from "@ohos/common/src/main/ets/dialog/ToastUtils"; +import { UniIconEntity, UniVersionEntity, UserProfile } from "@ohos/common/src/main/ets/viewmodel/DataBean"; +import { USER_PROFILE } from "@ohos/common/src/main/ets/constants/AppConstants"; +import { downStartUp, downStartUpForPath } from "@ohos/common/src/main/ets/utils/UniMpManager"; +import { Logger } from "@ohos/common"; +import { setUniMpNeedDownload } from "@ohos/common/src/main/ets/utils/KVManager"; +import { GlobalDownloadingDialog } from '@ohos/common/src/main/ets/dialog/GlobalDownloadingDialog'; +import { UpdateTipsDialog } from "@ohos/common/src/main/ets/dialog/UpdateTipsDialog"; +import { openAppStore, getAppVersion } from '@ohos/common/src/main/ets/utils/AppUtils'; +import { common } from "@kit.AbilityKit"; +import { StringUtils } from "@ohos/common/src/main/ets/utils/StringUtils"; +import emitter from "@ohos.events.emitter"; +import { EVT_USER_CONFIG_PROFILE } from "@ohos/common/src/main/ets/constants/EventConstants"; + +@Component +export struct HomeComponent { + @Consume('pageInfos') pageStack: NavPathStack; + @State resizeItems: HomeItem[] = HomeViewModel.getResizeData(); + @State certificateItems: HomeItem[] = HomeViewModel.getCertificateData(); + @State otherItems: HomeItem[] = HomeViewModel.getOtherData(); + + + @State profile: UserProfile | undefined = undefined; + + // UniMp 相关状态 + @State uniMpVersions: UniVersionEntity[] = [] + @State downloadWxProgress: number = 0 + @State downloadAlipayProgress: number = 0 + @State downloadWxStatus: number = 0 + @State downloadAlipayStatus: number = 0 + @State private uniMpDatas: UniIconEntity[] = [] + @State downloadProgress: number = 0; + + // 版本信息 相关状态 + @State targetVersion: string = ''; + @State isForceUpdate: boolean = false; + + @State uniMpEnable: boolean = false + itemPadding: number = 6; + + context = getContext(this) as common.UIAbilityContext; + + // 版本更新提示 + appUpdateTipsController: CustomDialogController = new CustomDialogController({ + builder: UpdateTipsDialog({ + title: '新版本提示', + version: 'v' + this.targetVersion, + desc: this.profile?.config?.versionEntity?.description?? '', + confirmText: '立即更新', + cancelText: '暂不', + isForce: this.isForceUpdate, + cancel: () => { console.info('用户点击取消') }, + confirm: () => { + openAppStore(this.context) + } + }), + alignment: DialogAlignment.Center, // 居中显示 + customStyle: true, // 使用自定义样式,去掉系统默认背景 + autoCancel: !this.isForceUpdate, // 点击遮罩层可关闭 + onWillDismiss: (dismissDialogAction: DismissDialogAction) => { + // DismissReason.PRESS_BACK 表示物理返回键或手势滑动返回 + if (dismissDialogAction.reason === DismissReason.PRESS_BACK) { + if (this.isForceUpdate) { // 如果是强制更新,不执行 dismiss(),从而拦截返回行为 + console.info('强制更新状态,禁用手势返回'); + } else { // 非强制更新则允许关闭 + dismissDialogAction.dismiss(); + } + } + } + }) + // UniMp下载进度提示 + downloadingController: CustomDialogController = new CustomDialogController({ + builder: GlobalDownloadingDialog({ + progress: this.downloadProgress, + }), + alignment: DialogAlignment.Center, // 居中显示 + customStyle: true, // 使用自定义样式,去掉系统默认背景 + autoCancel: true // 点击遮罩层可关闭 + }) + + uniMpMpDeal(){ + getAppVersion().then(version => { + // 版本更新,目标版本 + this.targetVersion = this.profile?.config?.versionEntity?.version?? '' + this.isForceUpdate = this.profile?.config?.versionEntity?.force?? false + + // 更新:1.有更新弹出更新提示确认框 2.没更新则提示无更新 + if(StringUtils.checkVersionLegal(version, this.targetVersion) && StringUtils.isNewVersionAvailable(version, this.targetVersion)){ + this.appUpdateTipsController.open() + } + }); + this.profile = AppStorage.get(USER_PROFILE); + // 模拟器 + this.uniMpVersions = this.profile?.config?.uniVersionList || []; + this.uniMpDatas = this.profile?.config?.homeIconEntity || []; + + this.uniMpEnable = this.profile?.config?.isUniMpOpen?? false + } + + aboutToAppear() { + this.uniMpMpDeal() + + // 事件监听 + emitter.on({ eventId: EVT_USER_CONFIG_PROFILE }, (eventData: emitter.EventData) => { + const payload = eventData.data; + if (payload) { + const result = payload["result"] as boolean; + if (result === true) { + //记得一定要延迟更新,否则接口未请求完成 + setTimeout(() => { + this.uniMpMpDeal() + }, 1000); + } + } + }); + } + + build() { + // 父容器 Column 默认从顶部开始排列 + Column() { + List() { + // 顶部图片 + ListItem() { + Image($r('app.media.ic_home_top_image')).width('100%') + } + if(this.uniMpEnable){ + //模拟器(2位) + ListItem() { + Column(){ + //模拟器标题 + Image($r('app.media.ic_home_unimp_title_icon')).width('67vp').height('26vp') + + //模拟器支付宝、微信 + Row({ space: 7 }){ + Stack(){ + Image($r('app.media.ic_unimp_wx_pld')) + .width('100%') + .onClick(()=> { + uniMpMpDeal(this.getUIContext(), this.uniMpVersions, 0, (status, progress, path) => { + this.downloadWxStatus = status + if(status===1){ + this.downloadWxProgress = progress + } + if(status === 2){ + // 下载完成,记录版本信息和下载路径 + let uniMpVersion = this.uniMpVersions[0] + setUniMpNeedDownload(uniMpVersion.unimp_id??'', uniMpVersion.unimp_type??'', uniMpVersion.version??'', path??'') + }else if (status === 3){ + // 已启动(运行) + } + + Logger.info('uniMp', 'downloadWxProgress: ' + this.downloadWxProgress + ', path: ' + path) + }) + }) + if(this.downloadWxStatus === 1){ + Stack(){ + Column(){ + Progress({ + value: this.downloadWxProgress, + total: 100, + type: ProgressType.Linear + }) + .width('100%') + .height(10) + .color('#ff4f8af6') + .backgroundColor('#ebebeb') + .style({ strokeWidth: 10, enableSmoothEffect: true }) + + Text('下载中,请稍后:' + this.downloadWxProgress + '%').fontColor(Color.White).fontSize(12).margin({ top: 4}) + } + .margin({ left:8, right:8 }) + } + .width('100%') + .height('100%') + .backgroundColor('#99000000') + .borderRadius(12) + } + } + .width('100%') + .aspectRatio(168/96) + .layoutWeight(1) + + + Stack(){ + Image($r('app.media.ic_unimp_alipay_pld')) + .width('100%') + .aspectRatio(168/96) + .layoutWeight(1) + .onClick(()=> { + uniMpMpDeal(this.getUIContext(), this.uniMpVersions, 1, (status, progress, path) => { + this.downloadAlipayStatus = status + if(status===1){ + this.downloadAlipayProgress = progress + } + if(status === 2){ + // 下载完成,记录版本信息和下载路径 + let uniMpVersion = this.uniMpVersions[1] + setUniMpNeedDownload(uniMpVersion.unimp_id??'', uniMpVersion.unimp_type??'', uniMpVersion.version??'', path??'') + }else if (status === 3){ + // 已启动(运行) + } + + Logger.info('uniMp', 'downloadAlipayProgress: ' + this.downloadAlipayProgress + ', path: ' + path) + }) + }) + if(this.downloadAlipayStatus === 1){ + Stack(){ + Column(){ + Progress({ + value: this.downloadAlipayProgress, + total: 100, + type: ProgressType.Linear + }) + .width('100%') + .height(10) + .color('#ff4f8af6') + .backgroundColor('#ebebeb') + .style({ strokeWidth: 10, enableSmoothEffect: true }) + + Text('下载中,请稍后:' + this.downloadAlipayProgress + '%').fontColor(Color.White).fontSize(12).margin({ top: 4}) + } + .margin({ left:8, right:8 }) + } + .width('100%') + .height('100%') + .backgroundColor('#99000000') + .borderRadius(12) + } + } + .width('100%') + .aspectRatio(168/96) + .layoutWeight(1) + } + .width('100%') + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor($r('app.color.normal_background')) + .borderRadius(18) + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -(this.itemPadding * 3) }) + // 如果希望整体列表都往上提,可以去掉 index 判断直接设为 -20 + .zIndex(1) // 确保列表项层级高于图片,实现“覆盖”效果 + + // 模拟器(3~4位) + ListItem() { + Grid() { + ForEach(this.uniMpDatas, (item: UniIconEntity) => { + GridItem() { + Column(){ + Image(item.icon).width(48).height(48) + + Text(item.text) + .width('100%') + .fontSize(12) + .margin({ top: 8}) + .backgroundColor($r('app.color.normal_background')) + .textAlign(TextAlign.Center) + .borderRadius(12) + } + .onClick(()=> { + this.downloadingController.open() + downStartUpForPath(this.getUIContext(), this.uniMpVersions, item, (status, progress, path) => { + Logger.info('uniMp', 'downStartUpForPath: ' + status + ', progress: ' + progress + ', path: ' + path) + if(status===1){ + this.downloadProgress = progress + }else{ + this.downloadingController.close() + } + }) + }) + } + }, (item: UniIconEntity) => item.url) + } + // 动态列数:数据 > 8 显示 3 列,否则 4 列 + .columnsTemplate(this.uniMpDatas.length > 8 ? '1fr 1fr 1fr' : '1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + // 由内容撑开高度,禁用 Grid 内部滚动,交给外层 List 处理 + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + // 如果 Grid 不设置高度,它会尝试占满。在 List 中建议根据行数计算高度 + // 或者直接不设高度,依赖内部子组件高度和 Gap + .height(this.calculateUniMpGridHeight()) + } + .offset({ y: -this.itemPadding }) + } + + if(this.uniMpEnable){ + // 选尺寸制作(Resize) + ListItem() { + Column() { + //选尺寸制作标题 + Image($r('app.media.ic_home_cute_title_icon')).width('101vp').height('26vp') + //选尺寸制作内容 + Grid() { + ForEach(this.resizeItems, (item: HomeItem) => { + GridItem() { + Stack(){ + Image(item.icon).width('100%').layoutWeight(1) + + Column() { + Text(item.name) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + + Text(item.resize) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + } + .padding({ left: '40%' }) + .align(Alignment.Start) + } + .onClick(() => { + const params: CuteParams = { + id: item.id, + name: "抠图尺寸", + width: item.width, + height: item.height, + }; + this.pageStack.pushPathByName("CuteLayout", params); + }) + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateResizeGridHeight()) + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor($r('app.color.normal_background')) + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -this.itemPadding }) + }else{ + // 选尺寸制作(Resize) + ListItem() { + Column() { + //选尺寸制作标题 + Image($r('app.media.ic_home_cute_title_icon')).width('101vp').height('26vp') + //选尺寸制作内容 + Grid() { + ForEach(this.resizeItems, (item: HomeItem) => { + GridItem() { + Stack(){ + Image(item.icon).width('100%').layoutWeight(1) + + Column() { + Text(item.name) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + + Text(item.resize) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + } + .padding({ left: '40%' }) + .align(Alignment.Start) + } + .onClick(() => { + const params: CuteParams = { + id: item.id, + name: "抠图尺寸", + width: item.width, + height: item.height, + }; + this.pageStack.pushPathByName("CuteLayout", params); + }) + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateResizeGridHeight()) + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor($r('app.color.normal_background')) + .borderRadius(18) + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -(this.itemPadding * 3) }) + // 如果希望整体列表都往上提,可以去掉 index 判断直接设为 -20 + .zIndex(1) // 确保列表项层级高于图片,实现“覆盖”效果 + } + + // 选证件制作 + ListItem() { + Column() { + //选尺寸制作标题 + Image($r('app.media.ic_home_certificate_title_icon')).width('102vp').height('26vp') + //选尺寸制作内容 + Grid() { + ForEach(this.certificateItems, (item: HomeItem) => { + GridItem() { + Column() { + Column(){ + Text(item.name) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Center) + .borderRadius(12) + + Text(item.resize) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Center) + .margin({ top: 4}) + .borderRadius(12) + } + } + .height('50vp') + .align(Alignment.Center) + .layoutWeight(1) + .borderRadius(10) + .linearGradient({ + angle: 180, // 渐变角度,180度表示从上到下 + colors: [ + ['#FFEEFFCC', 0.0], // [颜色, 偏移量0.0-1.0] + ['#FFFFFFFF', 1.0] + ] + }) + .shadow({ + radius: 3, // 阴影模糊半径 + color: '#1A000000', // 阴影颜色(建议带透明度) + offsetX: 0, // X轴偏移 + offsetY: 3 // Y轴偏移 + }) + .alignItems(HorizontalAlign.Center) // 设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Center) // 设置子组件垂直靠顶对齐(默认即是) + .onClick(() => { + const params: CuteParams = { + id: item.id, + name: item.name, + width: item.width, + height: item.height, + }; + this.pageStack.pushPathByName("CuteLayout", params); + }) + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateCertificateGridHeight()) + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor($r('app.color.normal_background')) + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: this.uniMpEnable?-this.itemPadding : -this.itemPadding * 4 }) + + // 其他 + ListItem() { + Column() { + //其他标题 + Image($r('app.media.ic_home_other_title')).width('51vp').height('26vp') + //其他内容 + Grid() { + ForEach(this.otherItems, (item: HomeItem) => { + GridItem() { + Column(){ + Image(item.icon).width(34).height(34) + + Text(item.name) + .width('100%') + .fontSize(12) + .margin({ top: 8}) + .backgroundColor($r('app.color.normal_background')) + .textAlign(TextAlign.Center) + .borderRadius(12) + } + .onClick(()=>{ + if("拼长图" === item.name){ + this.pageStack.pushPathByName("LongImageLayout", null); + }else if("格式转换" === item.name){ + this.pageStack.pushPathByName("FormatLayout", null); + }else if("改尺寸" === item.name){ + this.pageStack.pushPathByName("ResizeLayout", null); + }else if("拍照指南" === item.name){ + this.pageStack.pushPathByName("GuideLayout", null); + } + + }) + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateOtherGridHeight()) + .padding({ top: 12}) + } + .padding({ left: 16, right: 16}) + .backgroundColor($r('app.color.normal_background')) + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: this.uniMpEnable?-this.itemPadding * 6 : -this.itemPadding * 8 }) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.None) // 禁用回弹效果,使其更像普通布局 + } + .backgroundColor($r('app.color.normal_background')) + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Start) // 确保在父容器中靠顶 + } + + // 计算 Grid 需要的高度:(行数 * 每行高度) + (行数-1 * 间距) + private calculateUniMpGridHeight(): number { + const columns = this.uniMpDatas.length > 8 ? 3 : 4; + const rows = Math.ceil(this.uniMpDatas.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } + private calculateResizeGridHeight(): number { + const columns = 2; + const rows = Math.ceil(this.resizeItems.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } + private calculateCertificateGridHeight(): number { + const columns = 4; + const rows = Math.ceil(this.certificateItems.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } + private calculateOtherGridHeight(): number { + const columns = 4; + const rows = Math.ceil(this.otherItems.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } +} + + +function uniMpMpDeal(context: UIContext, uniMpVersions: UniVersionEntity[], index: number, callback: (status: number, progress: number, path?: string) => void) { + if (uniMpVersions && uniMpVersions.length > index) { + let uniMpVersion = uniMpVersions[index] + downStartUp(context, uniMpVersion, callback) + }else{ + ToastUtils.showToast(context, '出错了~'); + } +} + +/* +@Component +export struct HomeComponent { + @State resizeItems: HomeItem[] = HomeViewModel.getResizeData(); + @State certificateItems: HomeItem[] = HomeViewModel.getCertificateData(); + @State otherItems: HomeItem[] = HomeViewModel.getOtherData(); + + @State private unimpDatas: UniMpItem[] = [ + { id: '1', name: '机票', icon: $r('app.media.ic_unimp_fly_icon') }, + { id: '2', name: '火车票', icon: $r('app.media.ic_unimp_train_icon') }, + { id: '3', name: '工资单', icon: $r('app.media.ic_unimp_payroll_icon') }, + { id: '4', name: '视频群聊', icon: $r('app.media.ic_unimp_group_icon') } + ]; + + build() { + // 父容器 Column 默认从顶部开始排列 + Column() { + List() { + // 顶部图片 + ListItem() { + Image($r('app.media.ic_home_top_image')).width('100%') + } + //模拟器(2位) + ListItem() { + Column(){ + //模拟器标题 + Image($r('app.media.ic_home_unimp_title_icon')).width('67vp').height('26vp') + //模拟器支付宝、微信 + Row({ space: 7 }){ + Image($r('app.media.ic_unimp_wx_pld')).width('100%').layoutWeight(1) + Image($r('app.media.ic_unimp_alipay_pld')).width('100%').layoutWeight(1) + } + .width('100%') + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor('#F1F3F5') + .borderRadius(18) + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 【关键修复】设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 【可选】设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -18 }) + // 如果希望整体列表都往上提,可以去掉 index 判断直接设为 -20 + .zIndex(1) // 确保列表项层级高于图片,实现“覆盖”效果 + + // 模拟器(3~4位) + ListItem() { + Grid() { + ForEach(this.unimpDatas, (item: UniMpItem) => { + GridItem() { + Column(){ + Image(item.icon).width(48).height(48) + + Text(item.name) + .width('100%') + .fontSize(12) + .margin({ top: 8}) + .backgroundColor('#F1F3F5') + .textAlign(TextAlign.Center) + .borderRadius(12) + } + } + }, (item: UniMpItem) => item.id.toString()) + } + // 【关键逻辑】动态列数:数据 > 8 显示 3 列,否则 4 列 + .columnsTemplate(this.unimpDatas.length > 8 ? '1fr 1fr 1fr' : '1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + // 【关键】由内容撑开高度,禁用 Grid 内部滚动,交给外层 List 处理 + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + // 如果 Grid 不设置高度,它会尝试占满。在 List 中建议根据行数计算高度 + // 或者直接不设高度,依赖内部子组件高度和 Gap + .height(this.calculateUniMpGridHeight()) + } + .offset({ y: -6 }) + + // 选尺寸制作(Resize) + ListItem() { + Column() { + //选尺寸制作标题 + Image($r('app.media.ic_home_cute_title_icon')).width('101vp').height('26vp') + //选尺寸制作内容 + Grid() { + ForEach(this.resizeItems, (item: HomeItem) => { + GridItem() { + Stack(){ + Image(item.icon).width('100%').layoutWeight(1) + + Column() { + Text(item.name) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + + Text(item.resize) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + } + .padding({ left: '40%' }) + .align(Alignment.Start) + + } + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateResizeGridHeight()) + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor('#F1F3F5') + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 【关键修复】设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 【可选】设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -6 }) + + // 选证件制作 + ListItem() { + Column() { + //选尺寸制作标题 + Image($r('app.media.ic_home_certificate_title_icon')).width('102vp').height('26vp') + //选尺寸制作内容 + Grid() { + ForEach(this.certificateItems, (item: HomeItem) => { + GridItem() { + Column() { + Column(){ + Text(item.name) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Center) + .borderRadius(12) + + Text(item.resize) + .width('100%') + .fontSize(12) + .textAlign(TextAlign.Center) + .margin({ top: 4}) + .borderRadius(12) + } + } + .height('50vp') + .align(Alignment.Center) + .layoutWeight(1) + .borderRadius(10) + .linearGradient({ + angle: 180, // 渐变角度,180度表示从上到下 + colors: [ + ['#FFEEFFCC', 0.0], // [颜色, 偏移量0.0-1.0] + ['#FFFFFFFF', 1.0] + ] + }) + .alignItems(HorizontalAlign.Center) // 【关键修复】设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Center) // 【可选】设置子组件垂直靠顶对齐(默认即是) + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateCertificateGridHeight()) + .padding({ top: 12}) + } + .padding({ top: 12, left: 16, right: 16}) + .backgroundColor('#F1F3F5') + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 【关键修复】设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 【可选】设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -6 }) + + // 其他 + ListItem() { + Column() { + //其他标题 + Image($r('app.media.ic_home_other_title')).width('51vp').height('26vp') + //其他内容 + Grid() { + ForEach(this.otherItems, (item: HomeItem) => { + GridItem() { + Column(){ + Image(item.icon).width(34).height(34) + + Text(item.name) + .width('100%') + .fontSize(12) + .margin({ top: 8}) + .backgroundColor('#F1F3F5') + .textAlign(TextAlign.Center) + .borderRadius(12) + } + } + }, (item: HomeItem) => item.id.toString()) + } + .columnsTemplate('1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width('100%') + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.SELF_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + .height(this.calculateOtherGridHeight()) + .padding({ top: 12}) + } + .padding({ left: 16, right: 16}) + .backgroundColor('#F1F3F5') + .width('100%') // 必须设置宽度,否则无法感知“左侧”在哪里 + .alignItems(HorizontalAlign.Start) // 【关键修复】设置子组件水平靠左对齐 + .justifyContent(FlexAlign.Start) // 【可选】设置子组件垂直靠顶对齐(默认即是) + } + .offset({ y: -36 }) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.None) // 禁用回弹效果,使其更像普通布局 + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Start) // 确保在父容器中靠顶 + } + + // 计算 Grid 需要的高度:(行数 * 每行高度) + (行数-1 * 间距) + private calculateUniMpGridHeight(): number { + const columns = this.unimpDatas.length > 8 ? 3 : 4; + const rows = Math.ceil(this.unimpDatas.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } + private calculateResizeGridHeight(): number { + const columns = 2; + const rows = Math.ceil(this.resizeItems.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } + private calculateCertificateGridHeight(): number { + const columns = 4; + const rows = Math.ceil(this.certificateItems.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } + private calculateOtherGridHeight(): number { + const columns = 4; + const rows = Math.ceil(this.otherItems.length / columns); + return (rows * 80) + ((rows - 1) * 10); + } +} +*/ \ No newline at end of file diff --git a/products/app/src/main/ets/view/MineComponent.ets b/products/app/src/main/ets/view/MineComponent.ets new file mode 100644 index 0000000..248dcf7 --- /dev/null +++ b/products/app/src/main/ets/view/MineComponent.ets @@ -0,0 +1,318 @@ +import { commonConstants } from "main_layout/src/main/ets/constants/CommonConstants"; +import { common } from "@kit.AbilityKit"; +import { FILING_NO, USER_INFO, USER_PROFILE, WXAPPID } from "@ohos/common/src/main/ets/constants/AppConstants"; +import { KVStore } from "@ohos/common/src/main/ets/utils/KVStore"; +import { StringUtils } from '@ohos/common/src/main/ets/utils/StringUtils'; +import { openAppStore,getAppVersion } from '@ohos/common/src/main/ets/utils/AppUtils'; +import { MainService } from "main_layout/src/main/ets/viewmodel/MainService"; +import { ServiceProfile } from "main_layout/src/main/ets/viewmodel/DataBean"; +import { routerLogin } from '@ohos/common/src/main/ets/router/RouterManager'; +import { UserInfo, UserProfile } from "@ohos/common/src/main/ets/viewmodel/DataBean"; +import { emitter } from "@kit.BasicServicesKit"; +import * as wxopensdk from '@tencent/wechat_open_sdk'; +import { EVT_USER_INFO } from "@ohos/common/src/main/ets/constants/EventConstants"; +import { UpdateTipsDialog } from "@ohos/common/src/main/ets/dialog/UpdateTipsDialog"; +import { ToastUtils } from "@ohos/common/src/main/ets/dialog/ToastUtils"; +import BuildProfile from "BuildProfile"; + +@Component +export struct MineComponent { + @Consume('pageInfos') pageStack: NavPathStack; + @State appVersion: string = ''; + @State targetVersion: string = ''; + @State userInfo: UserInfo | undefined = undefined; + @State userProfile: UserProfile | undefined = undefined; + @State serviceProfile: ServiceProfile | undefined = undefined; + @State isUpdate: boolean = false; + @State buildTime: string = ''; + + context = getContext(this) as common.UIAbilityContext; + + appUpdateTipsController: CustomDialogController = new CustomDialogController({ + builder: UpdateTipsDialog({ + title: '新版本提示', + version: 'v' + this.targetVersion, + desc: this.userProfile?.config?.versionEntity?.description?? '', + confirmText: '立即更新', + cancelText: '暂不', + cancel: () => { console.info('用户点击取消') }, + confirm: () => { + openAppStore(this.context) + } + }), + alignment: DialogAlignment.Center, // 居中显示 + customStyle: true, // 使用自定义样式,去掉系统默认背景 + autoCancel: true // 点击遮罩层可关闭 + }) + + updateData(){ + KVStore.getInstance().get(USER_PROFILE, '{}').then(profileJson => { + this.userProfile = JSON.parse(profileJson) as UserProfile; + this.targetVersion = this.userProfile?.config?.versionEntity?.version??'0.0.0'; + + if(!StringUtils.checkVersionLegal(this.appVersion, this.targetVersion) || !StringUtils.isNewVersionAvailable(this.appVersion, this.targetVersion)){ + this.isUpdate = false; + }else{ + this.isUpdate = true; + } + }) + KVStore.getInstance().get(USER_INFO, '{}').then(userJson => { + this.userInfo = JSON.parse(userJson) as UserInfo; + }) + MainService.getService().then(res => { + this.serviceProfile = res; + }); + } + + aboutToAppear() { + getAppVersion().then(v => this.appVersion = v); + this.updateData() + this.buildTime = BuildProfile.DEBUG?BuildProfile.BUILD_TIME:''; + + // 事件监听 + emitter.on({ eventId: EVT_USER_INFO }, (eventData: emitter.EventData) => { + const payload = eventData.data; + if (payload) { + const result = payload["result"] as boolean; + if (result === true) { + //记得一定要延迟更新,否则接口未请求完成 + setTimeout(() => { + this.updateData() + }, 1000); + } + } + }); + } + + build() { + NavDestination() { + Stack() { + Image($r('app.media.ic_mine_top_mask_layer')).width('100%') + + Column() { + //头像部分 + Row() { + Image(this.userInfo ? this.userInfo.avater : $r('app.media.ic_mine_user_normal')) + .width(66) + .height(66) + .borderRadius(66) + .alt($r('app.media.ic_mine_user_normal')) + + Column(){ + Text(this.userInfo === undefined ? '游客' : (this.userInfo.name || '大白兔')) + .width('100%') + .fontSize(18) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + + Row(){ + Row(){ + Text(this.userInfo !== undefined ? 'ID:' + this.userInfo?.user_id : '登录体验更多功能哦~') + .fontSize(14) + .fontColor('#FF767676') + .textAlign(TextAlign.Start) + .margin({ top: 8}) + .borderRadius(12) + + if(this.userInfo !== undefined){ + Image($r('app.media.ic_mine_copy')) + .width('14vp') + .height('14vp') + .margin({ left: 8, top: 8}) + } + } + .onClick(() =>{ + //复制ID + StringUtils.copyText(this.userInfo?.user_id??'', true); + }) + } + .width('100%') + .align(Alignment.Start) + .justifyContent(FlexAlign.Start) + } + .padding({ left: 10}) + .onClick(() => { + // 跳转登录 + if(this.userInfo === undefined || this.userInfo.temp === true){ + routerLogin('mine',true) + } + }) + } + .width(commonConstants.rowWidth) + .height($r('app.float.arrow_item_height')) + + //列表部分 + Column() { + Row() { + Image($r('app.media.ic_mine_feedback')).width('19.5vp').height('21.51vp') + Text('意见反馈') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .padding({ left: 14}) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8}) + } + .width('100%') + .height('52vp') + .padding({ left: 14, right: 14}) + .onClick(() => { + this.pageStack.pushPathByName("FeedbackLayout", null); + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Image($r('app.media.ic_mine_customer')).width('19.5vp').height('21.51vp') + Text('在线客服') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .padding({ left: 14}) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8}) + } + .width('100%') + .height('52vp') + .padding({ left: 14, right: 14}) + .onClick(() => { + // 跳转客服 + const context = getContext(this) as common.UIAbilityContext; + jumpToWeChatCustomerService(context, this.serviceProfile?.corpid??'', this.serviceProfile?.address??'') + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Image($r('app.media.ic_mine_update')).width('19.5vp').height('21.51vp') + Text('版本更新') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .padding({ left: 14}) + .layoutWeight(1) + + Row(){ + + if(this.isUpdate){ + Badge({ + value: '', + style: { + badgeSize: 8, + badgeColor: Color.Red + }, + position: BadgePosition.RightTop + }) { + Text(BuildProfile.DEBUG?'V' + this.appVersion + '-' + 'debug' + '-' + this.buildTime + ' ' : 'V' + this.appVersion + ' ') + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + .padding({ right: 4, top: 4 }) + } + }else{ + Text(BuildProfile.DEBUG?'V' + this.appVersion + '-' + 'debug' + '-' + this.buildTime : 'V' + this.appVersion) + .fontSize(14) + .fontColor('#4d767676') + .textAlign(TextAlign.Center) + .borderRadius(12) + } + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8}) + } + } + .width('100%') + .height('52vp') + .padding({ left: 14, right: 14}) + .onClick(() => { + // 更新:1.有更新弹出更新提示确认框 2.没更新则提示无更新 + if(!StringUtils.checkVersionLegal(this.appVersion, this.targetVersion) || !StringUtils.isNewVersionAvailable(this.appVersion, this.targetVersion)){ + ToastUtils.normalToast({ + message: '已是最新版本', // 提示文字 + duration: 2000, // 显示时长 (ms),默认 1500 + bottom: '50%' + }); + }else{ + this.appUpdateTipsController.open() + } + }) + + Divider().height(0.5).backgroundColor('#4dd8d8d8').margin({ left: 14, right: 14}) + + Row() { + Image($r('app.media.ic_mine_setting')).width('19.5vp').height('21.51vp') + Text('设置') + .fontSize(16) + .fontColor('#FF1A1A1A') + .textAlign(TextAlign.Start) + .borderRadius(12) + .fontWeight(1) + .align(Alignment.Center) + .padding({ left: 14}) + .layoutWeight(1) + + Image($r('app.media.ic_mine_arrow_right')).width('14vp').height('14vp').margin({ left: 8}) + } + .width('100%') + .height('52vp') + .padding({ left: 14, right: 14}) + .onClick(() => { + this.pageStack.pushPathByName("SettingLayout", null); + }) + } + .backgroundColor(Color.White) + .width('100%') + .borderRadius(12) + .margin({ top: 30}) + .padding({ top: 6, bottom: 6}) + + Blank() + + //备案号 + Text(FILING_NO) + .width('100%') + .height(40) // 【核心修复 2】:高度改为固定值或 wrap_content,不能是 100% + .fontSize(10) + .fontColor('#FFAAAAAA') + .textAlign(TextAlign.Center) + .margin({ bottom: 86 }) // 留出底部边距,避免贴边 + } + .height('100%') + .padding({ left: 16, right: 16}) + .margin({ top: 88 }) + } + .alignContent(Alignment.TopStart) + .width('100%') + .height('100%') + } + .hideTitleBar(true) + } +} + +/** + * 跳转微信客服 + * @param context UIAbilityContext 上下文 + * @param corpId 企业ID + * @param kfUrl 客服URL + */ +function jumpToWeChatCustomerService(context: common.UIAbilityContext, corpId: string, address: string) { + const wxApi = wxopensdk.WXAPIFactory.createWXAPI(WXAPPID); + let req = new wxopensdk.OpenCustomerServiceChatReq(); + req.corpId = corpId; + req.url = address; + + wxApi.sendReq(context as common.UIAbilityContext, req) +} diff --git a/products/app/src/main/ets/viewmodel/DataBean.ets b/products/app/src/main/ets/viewmodel/DataBean.ets new file mode 100644 index 0000000..e5c7dc4 --- /dev/null +++ b/products/app/src/main/ets/viewmodel/DataBean.ets @@ -0,0 +1,16 @@ +/** + * 系统时间 的专属转换器 + */ +export const SystemTimeMapper = (raw: Object): string => { + console.info("RabbitLog_Mapper_Type: " + typeof raw); + if (typeof raw === 'string') { + return raw; + } + + if (typeof raw === 'object' && raw !== null) { + const dataField = (raw as Record)['data']; + return dataField ? String(dataField) : String(raw); + } + + return String(raw); +}; diff --git a/products/app/src/main/ets/viewmodel/HomeViewModel.ets b/products/app/src/main/ets/viewmodel/HomeViewModel.ets new file mode 100644 index 0000000..d063757 --- /dev/null +++ b/products/app/src/main/ets/viewmodel/HomeViewModel.ets @@ -0,0 +1,61 @@ +import { HomeItem } from "./LocalBean"; + +/** + * Binds data to components and provides interfaces. + */ +export class HomeViewModel { + /** + * 获取选尺寸制作数据 + * @return {Array} resizeItems + */ + getResizeData(): HomeItem[] { + const resizeItems: HomeItem[] = [ + { id: 11, name: '标准一寸', icon: $r('app.media.ic_resize_icon'),resize:'25x35mm',width: 25, height: 35 }, + { id: 12, name: '小一寸', icon: $r('app.media.ic_resize_icon'),resize:'22x32mm',width: 22, height: 32 }, + { id: 13, name: '标准二寸', icon: $r('app.media.ic_resize_icon'),resize:'35x53mm',width: 35, height: 53 }, + { id: 14, name: '小二寸', icon: $r('app.media.ic_resize_icon'),resize:'33x48mm',width: 33, height: 48 } + ]; + + return resizeItems; + } + + /** + * 获取选证件制作数据 + * @return {Array} resizeItems + */ + getCertificateData(): HomeItem[] { + const resizeItems: HomeItem[] = [ + { id: 21, name: '身份证', icon: $r('app.media.ic_placeholder'),resize:'26x32mm',width: 26, height: 32 }, + { id: 22, name: '护照', icon: $r('app.media.ic_placeholder'),resize:'33x48mm',width: 33, height: 48 }, + { id: 23, name: '驾驶证', icon: $r('app.media.ic_placeholder'),resize:'22x32mm',width: 22, height: 32 }, + { id: 24, name: '社保卡', icon: $r('app.media.ic_placeholder'),resize:'26x32mm',width: 26, height: 32 }, + { id: 25, name: '四六级', icon: $r('app.media.ic_placeholder'),resize:'26x32mm',width: 26, height: 32 }, + { id: 26, name: '司法考试', icon: $r('app.media.ic_placeholder'),resize:'33x48mm',width: 33, height: 48 }, + { id: 27, name: '结婚证', icon: $r('app.media.ic_placeholder'),resize:'40x60mm',width: 40, height: 60 }, + { id: 28, name: '中国签证', icon: $r('app.media.ic_placeholder'),resize:'35x45mm',width: 35, height: 45 } + ]; + + return resizeItems; + } + + /** + * 获取其他数据 + * @return {Array} resizeItems + */ + getOtherData(): HomeItem[] { + const resizeItems: HomeItem[] = [ + { id: 31, name: '拼长图', icon: $r('app.media.ic_home_long_image_icon'),resize:'',width: 0, height: 0 }, + { id: 32, name: '格式转换', icon: $r('app.media.ic_home_format_icon'),resize:'',width: 0, height: 0 }, + { id: 33, name: '改尺寸', icon: $r('app.media.ic_home_size_icon'),resize:'',width: 0, height: 0 }, + { id: 34, name: '拍照指南', icon: $r('app.media.ic_home_photo_guide_icon'),resize:'',width: 0, height: 0 } + ]; + + return resizeItems; + } + +} + + +const homeViewModel = new HomeViewModel(); + +export default homeViewModel; \ No newline at end of file diff --git a/products/app/src/main/ets/viewmodel/LocalBean.ets b/products/app/src/main/ets/viewmodel/LocalBean.ets new file mode 100644 index 0000000..177f54a --- /dev/null +++ b/products/app/src/main/ets/viewmodel/LocalBean.ets @@ -0,0 +1,26 @@ + +// 选尺寸制作 +export interface HomeItem { + id: number; + name: string; // 显示的文本内容 + icon: Resource; // 可选的图标资源 + resize: string; //尺寸值 + width: number; //尺寸值,宽度 + height: number; //尺寸值,高度 +} + +export class TabBarItem { + id: number = 0; + name: Resource = $r('app.string.tab_home'); + icon: Resource = $r('app.media.ic_tabs_home_normal'); + selectIcon: Resource = $r('app.media.ic_tabs_home_selected'); +} + +export interface DownloadItem { + id: number; + name: string; + url: string; + progress: number; + path: string; + state: number;//0:normal 1:down 2:finish +} \ No newline at end of file diff --git a/products/app/src/main/ets/viewmodel/TabsViewModel.ets b/products/app/src/main/ets/viewmodel/TabsViewModel.ets new file mode 100644 index 0000000..f126e2c --- /dev/null +++ b/products/app/src/main/ets/viewmodel/TabsViewModel.ets @@ -0,0 +1,29 @@ +import { commonConstants } from 'main_layout/src/main/ets/constants/CommonConstants'; +import { TabBarItem } from './LocalBean'; + +/** + * Binds data to components and provides interfaces. + */ +export class TabsViewModel { + /** + * Get item information for the tab. + * + * @return {Array} tabItems + */ + getTabData(): TabBarItem[] { + const tabItems: TabBarItem[] = []; + for (let i = 0; i < commonConstants.tabSize; i++) { + const itemInfo: TabBarItem = new TabBarItem(); + itemInfo.id = i; + itemInfo.name = i == 0 ? $r('app.string.tab_home') : $r('app.string.tab_mine'); + itemInfo.icon = i == 0 ? $r('app.media.ic_tabs_home_normal') : $r('app.media.ic_tabs_mine_normal'); + itemInfo.selectIcon = i == 0 ? $r('app.media.ic_tabs_home_selected') : $r('app.media.ic_tabs_mine_selected'); + tabItems.push(itemInfo); + } + return tabItems; + } +} + +const tabsViewModel = new TabsViewModel(); + +export default tabsViewModel; \ No newline at end of file diff --git a/products/app/src/main/ets/viewmodel/UserService.ets b/products/app/src/main/ets/viewmodel/UserService.ets new file mode 100644 index 0000000..d425572 --- /dev/null +++ b/products/app/src/main/ets/viewmodel/UserService.ets @@ -0,0 +1,49 @@ +import { SystemTimeMapper } from "./DataBean"; +import { ApiManager } from '@ohos/common/src/main/ets/provider/ApiManager'; +import { TimeSync } from "@ohos/common/src/main/ets/provider/RequestInterceptor"; + +export class UserService { + /** + * 获取用户资料 + * 内部会自动走 unsafeRetrofit 的签名和解密逻辑 + */ + // static async getUserInfo(userId: string): Promise { + // // 获取你在 initialize 中配置好的不安全实例 + // const service = ApiManager.getInstance().getUnsafeService(); + // + // // axios.get(url, config) + // // T 是返回数据的类型,R 是 AxiosResponse 的包装类型 + // const response = await service.get>('/api/v1/user/profile', { + // params: { "uid": userId } + // }); + // + // // 拦截器已经处理过解密和 BaseResponse 的解包 + // // 此时 response.data 已经是解密并 parse 后的 UserProfile 对象 + // return response.data.data; + // } + + /** + * 请求服务器事件 + */ + static async getSystemTime(): Promise { + const serverTime = await ApiManager.getInstance().requestForMapper( + '/api/time', + {}, + SystemTimeMapper + ); + // 【同步时间差】 + await TimeSync.sync(serverTime); + return serverTime; + } + + + + + + +// 场景 B:请求标准实体(Key 名完全对应,无需 Mapper) + // static async getSimpleProfile(): Promise { + // return await ApiManager.getInstance().request('/api/user/simple'); + // } + +} \ No newline at end of file diff --git a/products/app/src/main/module.json5 b/products/app/src/main/module.json5 new file mode 100644 index 0000000..60f2f54 --- /dev/null +++ b/products/app/src/main/module.json5 @@ -0,0 +1,78 @@ +{ + "module": { + "name": "app", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "AppAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "AppAbility", + "srcEntry": "./ets/appability/AppAbility.ets", + "description": "$string:ability_desc", + "icon": "$media:layered_image", + "label": "$string:app_name", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home", + "ohos.want.action.home", + "wxentity.action.open" + ] + }, + { + "actions": ["ohos.want.action.viewData"], + "uris": [ + { + "scheme": "wx7d1a7d1507482cef" // 微信 APPID + } + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "AppBackupAbility", + "srcEntry": "./ets/appbackupability/AppBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET", + }, + { + "name": "ohos.permission.APP_TRACKING_CONSENT", + "reason": "$string:reason_oaid", + "usedScene": { + "abilities": ["AppAbility"], + "when": "inuse" + } + } + ], + "querySchemes": [ + "weixin", //用于判断微信是否安装,开发者可通过 WXApi.isWXAppInstalled() 判断微信是否已安装; + "wxopensdk", //用于跳转微信. + "apmqpdispatch" //用于支付宝登录(极简SDK) + ] + } +} \ No newline at end of file diff --git a/products/app/src/main/resources/base/element/color.json b/products/app/src/main/resources/base/element/color.json new file mode 100644 index 0000000..b6e949a --- /dev/null +++ b/products/app/src/main/resources/base/element/color.json @@ -0,0 +1,16 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#F1F3F5" + }, + { + "name": "tab_font_color_normal", + "value": "#A7A7A7" + }, + { + "name": "tab_font_color_selected", + "value": "#333333" + } + ] +} \ No newline at end of file diff --git a/products/app/src/main/resources/base/element/float.json b/products/app/src/main/resources/base/element/float.json new file mode 100644 index 0000000..3abead3 --- /dev/null +++ b/products/app/src/main/resources/base/element/float.json @@ -0,0 +1,176 @@ +{ + "float": [ + { + "name": "catalogue_text_font_size", + "value": "16vp" + }, + { + "name": "catalogue_list_item_height", + "value": "40vp" + }, + { + "name": "catalogue_list_height", + "value": "168vp" + }, + { + "name": "catalogue_list_item_margin_bottom", + "value": "24vp" + }, + { + "name": "catalogue_list_item_border_radius", + "value": "20vp" + }, + { + "name": "catalogue_list_margin_left", + "value": "24vp" + }, + { + "name": "catalogue_list_margin_right", + "value": "24vp" + }, + { + "name": "system_capabilities_text_font_size", + "value": "16vp" + }, + { + "name": "tab_text_font_size", + "value": "10vp" + }, + { + "name": "arrow_width", + "value": "24vp" + }, + { + "name": "arrow_height", + "value": "24vp" + }, + { + "name": "arrow_left_borderRadius", + "value": "24vp" + }, + { + "name": "arrow_margin_left", + "value": "24vp" + }, + { + "name": "arrow_item_height", + "value": "56vp" + }, + { + "name": "grid_margin_left", + "value": "24vp" + }, + { + "name": "grid_margin_right", + "value": "24vp" + }, + { + "name": "grid_column_gap", + "value": "12vp" + }, + { + "name": "grid_row_gap", + "value": "12vp" + }, + { + "name": "grid_text_top", + "value": "8vp" + }, + { + "name": "grid_text_font_size", + "value": "14vp" + }, + { + "name": "grid_text_padding_left", + "value": "4vp" + }, + { + "name": "grid_row_border_radius", + "value": "12vp" + }, + { + "name": "tab_img_width", + "value": "24vp" + }, + { + "name": "tab_img_height", + "value": "24vp" + }, + { + "name": "system_capabilities_arrow_positionX", + "value": "24vp" + }, + { + "name": "system_capabilities_img_sm_width", + "value": "72vp" + }, + { + "name": "system_capabilities_img_md_width", + "value": "72vp" + }, + { + "name": "system_capabilities_img_lg_width", + "value": "90vp" + }, + { + "name": "system_capabilities_img_xl_width", + "value": "90vp" + }, + { + "name": "system_capabilities_img_sm_height", + "value": "72vp" + }, + { + "name": "system_capabilities_img_md_height", + "value": "72vp" + }, + { + "name": "system_capabilities_img_lg_height", + "value": "90vp" + }, + { + "name": "system_capabilities_img_xl_height", + "value": "90vp" + }, + { + "name": "system_capabilities_margin_bottom", + "value": "8vp" + }, + { + "name": "system_capabilities_text_margin", + "value": "12vp" + }, + { + "name": "tab_text_sm_top", + "value": "4vp" + }, + { + "name": "tab_text_sm_left", + "value": "0vp" + }, + { + "name": "tab_text_md_top", + "value": "0vp" + }, + { + "name": "tab_text_md_left", + "value": "10vp" + }, + { + "name": "tab_text_lg_top", + "value": "4vp" + }, + { + "name": "tab_text_lg_left", + "value": "0vp" + }, + { + "name": "tab_text_xl_top", + "value": "4vp" + }, + { + "name": "tab_text_xl_left", + "value": "0vp" + } + ] +} \ No newline at end of file diff --git a/products/app/src/main/resources/base/element/string.json b/products/app/src/main/resources/base/element/string.json new file mode 100644 index 0000000..1a5a506 --- /dev/null +++ b/products/app/src/main/resources/base/element/string.json @@ -0,0 +1,48 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "项目主模块(App模块)" + }, + { + "name": "ability_desc", + "value": "主页面能力" + }, + { + "name": "ability_label", + "value": "截图兔" + }, + { + "name": "grid_title_label", + "value": "title" + }, + { + "name": "adaptive_layout", + "value": "adaptive layout" + }, + { + "name": "system_cap", + "value": "system capabilities" + }, + { + "name": "responsive_layout", + "value": "responsive layout" + }, + { + "name": "location_cap_invalid", + "value": "This device does not have the system capability of location service" + }, + { + "name": "location_cap_valid", + "value": "This device has the system capability of location service" + }, + { + "name": "reason_read_image", + "value": "我们需要读取您的相册以选择图片完成拼图。" + }, + { + "name": "reason_oaid", + "value": "用于创建唯一的oaid服务标识。" + } + ] +} \ No newline at end of file diff --git a/products/app/src/main/resources/base/media/background.png b/products/app/src/main/resources/base/media/background.png new file mode 100644 index 0000000..923f2b3 Binary files /dev/null and b/products/app/src/main/resources/base/media/background.png differ diff --git a/products/app/src/main/resources/base/media/foreground.png b/products/app/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000..8f8a39a Binary files /dev/null and b/products/app/src/main/resources/base/media/foreground.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_alipay_auth_icon.png b/products/app/src/main/resources/base/media/ic_login_alipay_auth_icon.png new file mode 100644 index 0000000..62ab16b Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_alipay_auth_icon.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_alipay_icon.png b/products/app/src/main/resources/base/media/ic_login_alipay_icon.png new file mode 100644 index 0000000..a5d5433 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_alipay_icon.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_input_div_line.png b/products/app/src/main/resources/base/media/ic_login_input_div_line.png new file mode 100644 index 0000000..2b2335c Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_input_div_line.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_mask.png b/products/app/src/main/resources/base/media/ic_login_mask.png new file mode 100644 index 0000000..0fee5a3 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_mask.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_other_left_line.png b/products/app/src/main/resources/base/media/ic_login_other_left_line.png new file mode 100644 index 0000000..14c1d06 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_other_left_line.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_other_right_line.png b/products/app/src/main/resources/base/media/ic_login_other_right_line.png new file mode 100644 index 0000000..ce6b471 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_other_right_line.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_phone_icon.png b/products/app/src/main/resources/base/media/ic_login_phone_icon.png new file mode 100644 index 0000000..145e082 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_phone_icon.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_vertical_line.png b/products/app/src/main/resources/base/media/ic_login_vertical_line.png new file mode 100644 index 0000000..3d3992f Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_vertical_line.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_wx_auth_icon.png b/products/app/src/main/resources/base/media/ic_login_wx_auth_icon.png new file mode 100644 index 0000000..de940ed Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_wx_auth_icon.png differ diff --git a/products/app/src/main/resources/base/media/ic_login_wx_icon.png b/products/app/src/main/resources/base/media/ic_login_wx_icon.png new file mode 100644 index 0000000..2422195 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_login_wx_icon.png differ diff --git a/products/app/src/main/resources/base/media/ic_splash_logo.png b/products/app/src/main/resources/base/media/ic_splash_logo.png new file mode 100644 index 0000000..d53f507 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_splash_logo.png differ diff --git a/products/app/src/main/resources/base/media/ic_splash_mask.png b/products/app/src/main/resources/base/media/ic_splash_mask.png new file mode 100644 index 0000000..11a4582 Binary files /dev/null and b/products/app/src/main/resources/base/media/ic_splash_mask.png differ diff --git a/products/app/src/main/resources/base/media/layered_image.json b/products/app/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000..fb49920 --- /dev/null +++ b/products/app/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/products/app/src/main/resources/base/media/startIcon.png b/products/app/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000..8f8a39a Binary files /dev/null and b/products/app/src/main/resources/base/media/startIcon.png differ diff --git a/products/app/src/main/resources/base/profile/backup_config.json b/products/app/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000..78f40ae --- /dev/null +++ b/products/app/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/products/app/src/main/resources/base/profile/main_pages.json b/products/app/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..0c2c1b3 --- /dev/null +++ b/products/app/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,7 @@ +{ + "src": [ + "pages/SplashScreenPage", + "pages/LoginPage", + "pages/Index" + ] +} diff --git a/products/app/src/main/resources/dark/element/color.json b/products/app/src/main/resources/dark/element/color.json new file mode 100644 index 0000000..266ecf4 --- /dev/null +++ b/products/app/src/main/resources/dark/element/color.json @@ -0,0 +1,16 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + }, + { + "name": "tab_font_color_normal", + "value": "#A7A7A7" + }, + { + "name": "tab_font_color_selected", + "value": "#333333" + } + ] + } \ No newline at end of file diff --git a/products/app/src/mock/mock-config.json5 b/products/app/src/mock/mock-config.json5 new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/products/app/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/products/app/src/ohosTest/ets/test/Ability.test.ets b/products/app/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..85c78f6 --- /dev/null +++ b/products/app/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // 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. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + 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); + }) + }) +} \ No newline at end of file diff --git a/products/app/src/ohosTest/ets/test/List.test.ets b/products/app/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..794c7dc --- /dev/null +++ b/products/app/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/products/app/src/ohosTest/module.json5 b/products/app/src/ohosTest/module.json5 new file mode 100644 index 0000000..1f056de --- /dev/null +++ b/products/app/src/ohosTest/module.json5 @@ -0,0 +1,11 @@ +{ + "module": { + "name": "app_test", + "type": "feature", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/products/app/src/test/List.test.ets b/products/app/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/products/app/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/products/app/src/test/LocalUnit.test.ets b/products/app/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/products/app/src/test/LocalUnit.test.ets @@ -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); + }); + }); +} \ No newline at end of file