diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..41f4e52 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "chat/demo/flutter_module"] + path = chat/demo/flutter_module + url = https://codeup.aliyun.com/zlxt/zlxt-front/Histar-Flutter.git diff --git a/atomic_x/oh-package-lock.json5 b/atomic_x/oh-package-lock.json5 index 37db29a..9479e93 100644 --- a/atomic_x/oh-package-lock.json5 +++ b/atomic_x/oh-package-lock.json5 @@ -7,16 +7,15 @@ "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { "@tencent/mmkv@1.3.5": "@tencent/mmkv@1.3.5", - "@tencentcloud/atomicxcore@4.1.0": "@tencentcloud/atomicxcore@4.1.0", + "@tencentcloud/atomicxcore@4.1.0": "@tencentcloud/atomicxcore@4.2.0", "@tencentcloud/imsdk@^8.6.7020": "@tencentcloud/imsdk@8.9.7542", "@tencentcloud/liteavsdk_professional@^13.2.1135": "@tencentcloud/liteavsdk_professional@13.2.8732", - "@tencentcloud/rtcroomengine@^4.1.0": "@tencentcloud/rtcroomengine@4.1.0", - "libTXCDownloadProxy.so@../application/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": "libTXCDownloadProxy.so@../application/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", - "libTXCThumbPlayer.so@../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": "libTXCThumbPlayer.so@../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", - "libliteavsdk.so@../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": "libliteavsdk.so@../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", - "librtcroomengine.so@../application/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine": "librtcroomengine.so@../application/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine", - "txcdownloadproxy@../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": "txcdownloadproxy@../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", - "txcthumbplayer@../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": "txcthumbplayer@../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har" + "libTXCDownloadProxy.so@../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": "libTXCDownloadProxy.so@../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", + "libTXCThumbPlayer.so@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": "libTXCThumbPlayer.so@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", + "libatomicengine.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": "libatomicengine.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", + "libliteavsdk.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": "libliteavsdk.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", + "txcdownloadproxy@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": "txcdownloadproxy@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", + "txcthumbplayer@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": "txcthumbplayer@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har" }, "packages": { "@tencent/mmkv@1.3.5": { @@ -30,15 +29,15 @@ }, "packageType": "InterfaceHar" }, - "@tencentcloud/atomicxcore@4.1.0": { + "@tencentcloud/atomicxcore@4.2.0": { "name": "@tencentcloud/atomicxcore", - "version": "4.1.0", - "integrity": "sha512-Hob9rrY3qkeraMguWJovDAKqtEI7gBx7e7GhcuXbtuOa7vxSaCvvUCpNf9KrymXXVM82WLtMOhgh2NCQdKJP/Q==", - "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.1.0.har", + "version": "4.2.0", + "integrity": "sha512-+4puTeVVcPmzuZ/nKtpZnWM9uOCPTLmtcvKQ2YgdP0TsXOSWAhC03TqyNM+UwVDZMSMEJF/2RbpSBN5HyBklaw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.2.0.har", "registryType": "ohpm", "dependencies": { + "libatomicengine.so": "file:./src/main/cpp/types/libatomicengine", "@tencentcloud/imsdk": "^8.6.7020", - "@tencentcloud/rtcroomengine": "^4.1.0", "@tencentcloud/liteavsdk_professional": "^13.2.1135" } }, @@ -60,54 +59,43 @@ "txcthumbplayer": "file:./libs/txcthumbplayer.har" } }, - "@tencentcloud/rtcroomengine@4.1.0": { - "name": "@tencentcloud/rtcroomengine", - "version": "4.1.0", - "integrity": "sha512-lspTdW4LmIdo4nvhUsr8362qwnzYSGkT0aENSx7014rKwxJ62ksTBIxkR/SEEe9PvTjVhg8aqKMb2YNx2wD9KA==", - "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/rtcroomengine/-/rtcroomengine-4.1.0.har", - "registryType": "ohpm", - "dependencies": { - "librtcroomengine.so": "file:./src/main/cpp/types/librtcroomengine", - "@tencentcloud/liteavsdk_professional": "^13.2.1135" - } - }, - "libTXCDownloadProxy.so@../application/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": { + "libTXCDownloadProxy.so@../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": { "name": "libTXCDownloadProxy.so", "version": "1.0.0", - "resolved": "../application/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", + "resolved": "../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", "registryType": "local" }, - "libTXCThumbPlayer.so@../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": { + "libTXCThumbPlayer.so@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": { "name": "libTXCThumbPlayer.so", "version": "0.0.0", - "resolved": "../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", + "resolved": "../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", "registryType": "local" }, - "libliteavsdk.so@../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { - "name": "libliteavsdk.so", + "libatomicengine.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": { + "name": "libatomicengine.so", "version": "1.0.0", - "resolved": "../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", + "resolved": "../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", "registryType": "local" }, - "librtcroomengine.so@../application/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine": { - "name": "librtcroomengine.so", + "libliteavsdk.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { + "name": "libliteavsdk.so", "version": "1.0.0", - "resolved": "../application/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine", + "resolved": "../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", "registryType": "local" }, - "txcdownloadproxy@../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": { + "txcdownloadproxy@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": { "name": "txcdownloadproxy", "version": "1.0.0-rc.1", - "resolved": "../application/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", + "resolved": "../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", "registryType": "local", "dependencies": { "libTXCDownloadProxy.so": "file:./src/main/cpp/types/libtxcdownloadproxy" } }, - "txcthumbplayer@../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": { + "txcthumbplayer@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": { "name": "txcthumbplayer", "version": "1.0.0-alpha.1", - "resolved": "../application/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har", + "resolved": "../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har", "registryType": "local", "dependencies": { "libTXCThumbPlayer.so": "file:./src/main/ets/napi_export", diff --git a/atomic_x/oh-package.json5 b/atomic_x/oh-package.json5 index 1b690f4..b0c2ba2 100644 --- a/atomic_x/oh-package.json5 +++ b/atomic_x/oh-package.json5 @@ -7,7 +7,11 @@ "license": "Apache-2.0", "packageType": "InterfaceHar", "dependencies": { + +// "@tencentcloud/atomicxcore": "^3.6.0", + "@tencentcloud/atomicxcore": "4.1.0", + "@tencent/mmkv": "1.3.5" } } \ No newline at end of file diff --git a/call/BuildProfile.ets b/call/BuildProfile.ets index 3a501e5..120d0e0 100644 --- a/call/BuildProfile.ets +++ b/call/BuildProfile.ets @@ -1,7 +1,7 @@ /** * 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 HAR_VERSION = '4.2.0'; export const BUILD_MODE_NAME = 'debug'; export const DEBUG = true; export const TARGET_NAME = 'default'; diff --git a/call/oh-package-lock.json5 b/call/oh-package-lock.json5 new file mode 100644 index 0000000..bf7294a --- /dev/null +++ b/call/oh-package-lock.json5 @@ -0,0 +1,94 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@tencentcloud/atomicxcore@^4.2.0": "@tencentcloud/atomicxcore@4.2.0", + "@tencentcloud/imsdk@^8.6.7020": "@tencentcloud/imsdk@8.9.7542", + "@tencentcloud/liteavsdk_professional@^13.2.1135": "@tencentcloud/liteavsdk_professional@13.2.8732", + "libTXCDownloadProxy.so@../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": "libTXCDownloadProxy.so@../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", + "libTXCThumbPlayer.so@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": "libTXCThumbPlayer.so@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", + "libatomicengine.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": "libatomicengine.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", + "libliteavsdk.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": "libliteavsdk.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", + "txcdownloadproxy@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": "txcdownloadproxy@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", + "txcthumbplayer@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": "txcthumbplayer@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har" + }, + "packages": { + "@tencentcloud/atomicxcore@4.2.0": { + "name": "@tencentcloud/atomicxcore", + "version": "4.2.0", + "integrity": "sha512-+4puTeVVcPmzuZ/nKtpZnWM9uOCPTLmtcvKQ2YgdP0TsXOSWAhC03TqyNM+UwVDZMSMEJF/2RbpSBN5HyBklaw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.2.0.har", + "registryType": "ohpm", + "dependencies": { + "libatomicengine.so": "file:./src/main/cpp/types/libatomicengine", + "@tencentcloud/imsdk": "^8.6.7020", + "@tencentcloud/liteavsdk_professional": "^13.2.1135" + } + }, + "@tencentcloud/imsdk@8.9.7542": { + "name": "@tencentcloud/imsdk", + "version": "8.9.7542", + "integrity": "sha512-lfvDkhuklD9NAL5jJ2fnt7Ml9dl8uqJC7VKKYmx0g2WrJBYchPZM4pKJv7uwEHSUuYpSbYVj6RAnPq+IM0A2JA==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/imsdk/-/imsdk-8.9.7542.har", + "registryType": "ohpm" + }, + "@tencentcloud/liteavsdk_professional@13.2.8732": { + "name": "@tencentcloud/liteavsdk_professional", + "version": "13.2.8732", + "integrity": "sha512-ud+x8AEWbtlRnvBi9cA+PE+FC+JcEqBsaJU7Kvu2kQBbD8L0u4z/Wg1ejmckWiHb8X+D/4K2goP5EeUP728t/A==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/liteavsdk_professional/-/liteavsdk_professional-13.2.8732.har", + "registryType": "ohpm", + "dependencies": { + "libliteavsdk.so": "file:./src/main/cpp/types/libliteavsdk", + "txcthumbplayer": "file:./libs/txcthumbplayer.har" + } + }, + "libTXCDownloadProxy.so@../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": { + "name": "libTXCDownloadProxy.so", + "version": "1.0.0", + "resolved": "../chat/demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", + "registryType": "local" + }, + "libTXCThumbPlayer.so@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": { + "name": "libTXCThumbPlayer.so", + "version": "0.0.0", + "resolved": "../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", + "registryType": "local" + }, + "libatomicengine.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": { + "name": "libatomicengine.so", + "version": "1.0.0", + "resolved": "../chat/demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", + "registryType": "local" + }, + "libliteavsdk.so@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { + "name": "libliteavsdk.so", + "version": "1.0.0", + "resolved": "../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", + "registryType": "local" + }, + "txcdownloadproxy@../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": { + "name": "txcdownloadproxy", + "version": "1.0.0-rc.1", + "resolved": "../chat/demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", + "registryType": "local", + "dependencies": { + "libTXCDownloadProxy.so": "file:./src/main/cpp/types/libtxcdownloadproxy" + } + }, + "txcthumbplayer@../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": { + "name": "txcthumbplayer", + "version": "1.0.0-alpha.1", + "resolved": "../chat/demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har", + "registryType": "local", + "dependencies": { + "libTXCThumbPlayer.so": "file:./src/main/ets/napi_export", + "txcdownloadproxy": "file:./libs/TXCDownloadProxy.har" + } + } + } +} \ No newline at end of file diff --git a/chat/demo/AppScope/app.json5 b/chat/demo/AppScope/app.json5 index 05951e7..46918af 100644 --- a/chat/demo/AppScope/app.json5 +++ b/chat/demo/AppScope/app.json5 @@ -8,3 +8,5 @@ "label": "$string:app_name" } } + + diff --git a/chat/demo/build-profile.json5 b/chat/demo/build-profile.json5 index f3581ea..68defe3 100644 --- a/chat/demo/build-profile.json5 +++ b/chat/demo/build-profile.json5 @@ -18,9 +18,27 @@ { "name": "debug" }, + { + "name": "profile" + }, { "name": "release" } + ], + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "/Users/shims/.ohos/config/default_demo_R2owGaqvlP_uwPh4dzHWHHUGoaVCDSjc0ldCbVlGoKg=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B5FBBE5E7D341C3E78DB1BC31EBA0E236514986DD1E5B39FD6F19A80297B19F788648AC7D53749F", + "profile": "/Users/shims/.ohos/config/default_demo_R2owGaqvlP_uwPh4dzHWHHUGoaVCDSjc0ldCbVlGoKg=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "/Users/shims/.ohos/config/default_demo_R2owGaqvlP_uwPh4dzHWHHUGoaVCDSjc0ldCbVlGoKg=.p12", + "storePassword": "0000001BE89C9E7E6844394EFD2E2D993130E37468D1E4863524DFAA5F46F9921369A456E6ADD2CAF69EE3" + } + } ] }, "modules": [ diff --git a/chat/demo/entry/build-profile.json5 b/chat/demo/entry/build-profile.json5 index 4d61187..7f205ec 100644 --- a/chat/demo/entry/build-profile.json5 +++ b/chat/demo/entry/build-profile.json5 @@ -1,9 +1,47 @@ { "apiType": "stageMode", "buildOption": { + // 默认配置(与 Android 默认 debug 行为一致:使用 test 环境) + "arkOptions": { + "buildProfileFields": { + "ENV_TYPE": "test", + "BASE_URL": "https://test.histar.com/zlxt/app/", + "H5_URL": "https://test.histar.com/histar-h5/#", + "IM_SDK_APPID": 1600111396, + "PUSH_APPKEY": "oWJmXCifmNL5oaRZJrQhlvcZpF9WuQccsRrC7HGUkONFUrpcSkOnrEWMvLPZc2aR" + } + } }, "buildOptionSet": [ { + // debug 模式:开发环境,默认与 Android `debug` 当前注释中启用的 test 环境一致; + // 如需切到 dev 真机/真服务,将下面 5 个字段值替换为 dev 配置即可。 + "name": "debug", + "arkOptions": { + "buildProfileFields": { + "ENV_TYPE": "test", + "BASE_URL": "https://test.histar.com/zlxt/app/", + "H5_URL": "https://test.histar.com/histar-h5/#", + "IM_SDK_APPID": 1600111396, + "PUSH_APPKEY": "oWJmXCifmNL5oaRZJrQhlvcZpF9WuQccsRrC7HGUkONFUrpcSkOnrEWMvLPZc2aR" + } + } + }, + { + // profile 模式:测试环境,对应 Android `profile` + "name": "profile", + "arkOptions": { + "buildProfileFields": { + "ENV_TYPE": "test", + "BASE_URL": "https://test.histar.com/zlxt/app/", + "H5_URL": "https://test.histar.com/histar-h5/#", + "IM_SDK_APPID": 1600111396, + "PUSH_APPKEY": "oWJmXCifmNL5oaRZJrQhlvcZpF9WuQccsRrC7HGUkONFUrpcSkOnrEWMvLPZc2aR" + } + } + }, + { + // release 模式:生产环境,对应 Android `release` "name": "release", "arkOptions": { "obfuscation": { @@ -13,9 +51,16 @@ "./obfuscation-rules.txt" ] } + }, + "buildProfileFields": { + "ENV_TYPE": "release", + "BASE_URL": "https://trip.histar.com/zlxt/app/", + "H5_URL": "https://trip.histar.com/histar-h5/#", + "IM_SDK_APPID": 1600105433, + "PUSH_APPKEY": "Z8CVHPKtVWQjk9RwktAlezAGZ8Igmh5BsZXLtRTBu5j0BeIRiE8mt7A0uYXfzkcY" } } - }, + } ], "targets": [ { @@ -25,4 +70,4 @@ "name": "ohosTest", } ] -} \ No newline at end of file +} diff --git a/chat/demo/entry/oh-package-lock.json5 b/chat/demo/entry/oh-package-lock.json5 index 48d945f..89b3186 100644 --- a/chat/demo/entry/oh-package-lock.json5 +++ b/chat/demo/entry/oh-package-lock.json5 @@ -8,23 +8,29 @@ "specifiers": { "@ohos/axios@2.1.1": "@ohos/axios@2.1.1", "@ohos/crypto-js@^2.0.1": "@ohos/crypto-js@2.0.5", + "@ohos/flutter_module@../flutter_module/build/ohos/har/release/flutter_module.har": "@ohos/flutter_module@../flutter_module/build/ohos/har/release/flutter_module.har", + "@ohos/flutter_ohos@../flutter_module/build/ohos/har/release/flutter_embedding_release.har": "@ohos/flutter_ohos@../flutter_module/build/ohos/har/release/flutter_embedding_release.har", "@tencent/mmkv@1.3.5": "@tencent/mmkv@1.3.5", "@tencentcloud/atomicx@../../../atomic_x": "@tencentcloud/atomicx@../../../atomic_x", - "@tencentcloud/atomicxcore@4.1.0": "@tencentcloud/atomicxcore@4.1.0", - "@tencentcloud/atomicxcore@^3.6.0": "@tencentcloud/atomicxcore@4.1.0", + "@tencentcloud/atomicxcore@4.1.0": "@tencentcloud/atomicxcore@4.2.0", + "@tencentcloud/atomicxcore@^3.6.0": "@tencentcloud/atomicxcore@4.2.0", + "@tencentcloud/atomicxcore@^4.2.0": "@tencentcloud/atomicxcore@4.2.0", "@tencentcloud/imsdk@^8.6.7020": "@tencentcloud/imsdk@8.9.7542", "@tencentcloud/liteavsdk_professional@^13.2.1135": "@tencentcloud/liteavsdk_professional@13.2.8732", - "@tencentcloud/rtcroomengine@^4.1.0": "@tencentcloud/rtcroomengine@4.1.0", + "@tencentcloud/tuicallkit@../../../call": "@tencentcloud/tuicallkit@../../../call", "chatuikit@../../uikit": "chatuikit@../../uikit", "dayjs@^1.11.7": "dayjs@1.11.13", + "flutter_boost@../flutter_module/build/ohos/har/release/flutter_boost.har": "flutter_boost@../flutter_module/build/ohos/har/release/flutter_boost.har", + "flutter_native_arm64_v8a@../flutter_module/build/ohos/har/release/arm64_v8a_release.har": "flutter_native_arm64_v8a@../flutter_module/build/ohos/har/release/arm64_v8a_release.har", + "image_picker_ohos@../flutter_module/build/ohos/har/release/image_picker_ohos.har": "image_picker_ohos@../flutter_module/build/ohos/har/release/image_picker_ohos.har", "libTXCDownloadProxy.so@../oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": "libTXCDownloadProxy.so@../oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", "libTXCThumbPlayer.so@../oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": "libTXCThumbPlayer.so@../oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", + "libatomicengine.so@../oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": "libatomicengine.so@../oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", "libliteavsdk.so@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": "libliteavsdk.so@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", - "librtcroomengine.so@../oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine": "librtcroomengine.so@../oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine", "pako@^2.1.0": "pako@2.1.0", - "tuicallkit@../../call": "tuicallkit@../../call", "txcdownloadproxy@../oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": "txcdownloadproxy@../oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", - "txcthumbplayer@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": "txcthumbplayer@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har" + "txcthumbplayer@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": "txcthumbplayer@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har", + "webview_flutter_ohos@../flutter_module/build/ohos/har/release/webview_flutter_ohos.har": "webview_flutter_ohos@../flutter_module/build/ohos/har/release/webview_flutter_ohos.har" }, "packages": { "@ohos/axios@2.1.1": { @@ -41,6 +47,25 @@ "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/crypto-js/-/crypto-js-2.0.5.har", "registryType": "ohpm" }, + "@ohos/flutter_module@../flutter_module/build/ohos/har/release/flutter_module.har": { + "name": "@ohos/flutter_module", + "version": "1.0.0", + "resolved": "../flutter_module/build/ohos/har/release/flutter_module.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "", + "flutter_native_arm64_v8a": "", + "flutter_boost": "", + "image_picker_ohos": "", + "webview_flutter_ohos": "" + } + }, + "@ohos/flutter_ohos@../flutter_module/build/ohos/har/release/flutter_embedding_release.har": { + "name": "@ohos/flutter_ohos", + "version": "1.0.0-e89c068aa5", + "resolved": "../flutter_module/build/ohos/har/release/flutter_embedding_release.har", + "registryType": "local" + }, "@tencent/mmkv@1.3.5": { "name": "@tencent/mmkv", "version": "1.3.5", @@ -63,15 +88,15 @@ }, "packageType": "InterfaceHar" }, - "@tencentcloud/atomicxcore@4.1.0": { + "@tencentcloud/atomicxcore@4.2.0": { "name": "@tencentcloud/atomicxcore", - "version": "4.1.0", - "integrity": "sha512-Hob9rrY3qkeraMguWJovDAKqtEI7gBx7e7GhcuXbtuOa7vxSaCvvUCpNf9KrymXXVM82WLtMOhgh2NCQdKJP/Q==", - "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.1.0.har", + "version": "4.2.0", + "integrity": "sha512-+4puTeVVcPmzuZ/nKtpZnWM9uOCPTLmtcvKQ2YgdP0TsXOSWAhC03TqyNM+UwVDZMSMEJF/2RbpSBN5HyBklaw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.2.0.har", "registryType": "ohpm", "dependencies": { + "libatomicengine.so": "file:./src/main/cpp/types/libatomicengine", "@tencentcloud/imsdk": "^8.6.7020", - "@tencentcloud/rtcroomengine": "^4.1.0", "@tencentcloud/liteavsdk_professional": "^13.2.1135" } }, @@ -93,15 +118,13 @@ "txcthumbplayer": "file:./libs/txcthumbplayer.har" } }, - "@tencentcloud/rtcroomengine@4.1.0": { - "name": "@tencentcloud/rtcroomengine", - "version": "4.1.0", - "integrity": "sha512-lspTdW4LmIdo4nvhUsr8362qwnzYSGkT0aENSx7014rKwxJ62ksTBIxkR/SEEe9PvTjVhg8aqKMb2YNx2wD9KA==", - "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/rtcroomengine/-/rtcroomengine-4.1.0.har", - "registryType": "ohpm", + "@tencentcloud/tuicallkit@../../../call": { + "name": "@tencentcloud/tuicallkit", + "version": "4.2.0", + "resolved": "../../../call", + "registryType": "local", "dependencies": { - "librtcroomengine.so": "file:./src/main/cpp/types/librtcroomengine", - "@tencentcloud/liteavsdk_professional": "^13.2.1135" + "@tencentcloud/atomicxcore": "^4.2.0" } }, "chatuikit@../../uikit": { @@ -122,6 +145,30 @@ "shasum": "92430b0139055c3ebb60150aa13e860a4b5a366c", "registryType": "ohpm" }, + "flutter_boost@../flutter_module/build/ohos/har/release/flutter_boost.har": { + "name": "flutter_boost", + "version": "4.6.5", + "resolved": "../flutter_module/build/ohos/har/release/flutter_boost.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "file:har/flutter.har" + } + }, + "flutter_native_arm64_v8a@../flutter_module/build/ohos/har/release/arm64_v8a_release.har": { + "name": "flutter_native_arm64_v8a", + "version": "1.0.0-e89c068aa5", + "resolved": "../flutter_module/build/ohos/har/release/arm64_v8a_release.har", + "registryType": "local" + }, + "image_picker_ohos@../flutter_module/build/ohos/har/release/image_picker_ohos.har": { + "name": "image_picker_ohos", + "version": "1.0.0", + "resolved": "../flutter_module/build/ohos/har/release/image_picker_ohos.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "file:libs/flutter.har" + } + }, "libTXCDownloadProxy.so@../oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": { "name": "libTXCDownloadProxy.so", "version": "1.0.0", @@ -134,16 +181,16 @@ "resolved": "../oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", "registryType": "local" }, - "libliteavsdk.so@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { - "name": "libliteavsdk.so", + "libatomicengine.so@../oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": { + "name": "libatomicengine.so", "version": "1.0.0", - "resolved": "../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", + "resolved": "../oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", "registryType": "local" }, - "librtcroomengine.so@../oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine": { - "name": "librtcroomengine.so", + "libliteavsdk.so@../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { + "name": "libliteavsdk.so", "version": "1.0.0", - "resolved": "../oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine", + "resolved": "../oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", "registryType": "local" }, "pako@2.1.0": { @@ -154,17 +201,6 @@ "shasum": "266cc37f98c7d883545d11335c00fbd4062c9a86", "registryType": "ohpm" }, - "tuicallkit@../../call": { - "name": "tuicallkit", - "version": "1.0.0", - "resolved": "../../call", - "registryType": "local", - "dependencies": { - "@tencentcloud/liteavsdk_professional": "^13.2.1135", - "@tencentcloud/imsdk": "^8.6.7020", - "@tencentcloud/rtcroomengine": "^4.1.0" - } - }, "txcdownloadproxy@../oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": { "name": "txcdownloadproxy", "version": "1.0.0-rc.1", @@ -183,6 +219,16 @@ "libTXCThumbPlayer.so": "file:./src/main/ets/napi_export", "txcdownloadproxy": "file:./libs/TXCDownloadProxy.har" } + }, + "webview_flutter_ohos@../flutter_module/build/ohos/har/release/webview_flutter_ohos.har": { + "name": "webview_flutter_ohos", + "version": "1.0.0", + "resolved": "../flutter_module/build/ohos/har/release/webview_flutter_ohos.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "", + "flutter_native_arm64_v8a": "" + } } } } \ No newline at end of file diff --git a/chat/demo/entry/oh-package.json5 b/chat/demo/entry/oh-package.json5 index a58f4a2..3a1547c 100644 --- a/chat/demo/entry/oh-package.json5 +++ b/chat/demo/entry/oh-package.json5 @@ -18,7 +18,14 @@ "dayjs": "^1.11.7", "pako": "^2.1.0", "@ohos/crypto-js": "^2.0.1", - "@tencentcloud/tuicallkit": "file:../../../call" + "@tencentcloud/tuicallkit": "file:../../../call", + + "flutter_boost": "", + "webview_flutter_ohos": "", + "image_picker_ohos": "", + "@ohos/flutter_ohos": "", + "flutter_native_arm64_v8a": "", + "@ohos/flutter_module": "" } } diff --git a/chat/demo/entry/src/main/ets/api/MessageApi.ets b/chat/demo/entry/src/main/ets/api/MessageApi.ets new file mode 100644 index 0000000..d69342c --- /dev/null +++ b/chat/demo/entry/src/main/ets/api/MessageApi.ets @@ -0,0 +1,46 @@ +/** + * 消息中心网络接口(对齐 Android `MessageCenterApi`)。 + */ +import { MessageDetailBean } from '../bean/message/MessageDetailBean'; +import { MessageResponse } from '../bean/message/MessageResponse'; +import { http } from '../network'; + +/** 消息列表查询参数,对应 GET /api/v1/msg/list 的 query */ +export interface MessageListQuery { + /** 当前页码,从 1 开始 */ + pageNum: number; + /** 每页条数 */ + pageSize: number; + /** 搜索关键字,空字符串表示不过滤 */ + keyword: string; +} + +/** + * 拉取消息中心分页列表。 + * @param query 分页与搜索条件 + * @returns 含 list / total 等字段的分页数据(Http 拦截器已解包 data) + */ +export const getMessageList = (query: MessageListQuery): Promise => { + const params: MessageListQuery = { + pageNum: query.pageNum, + pageSize: query.pageSize, + keyword: query.keyword, + }; + return http.get('/api/v1/msg/list', params); +}; + +/** + * 拉取单条消息详情(进入详情页时调用,服务端通常会标记已读)。 + * @param msgId 消息 id + */ +export const getMessageDetail = (msgId: string): Promise => { + return http.get(`/api/v1/msg/${msgId}`); +}; + +/** + * 删除单条消息(列表左滑删除确认后调用)。 + * @param msgId 消息 id + */ +export const deleteMessage = (msgId: string): Promise => { + return http.delete(`/api/v1/msg/${msgId}`); +}; diff --git a/chat/demo/entry/src/main/ets/api/SystemApi.ets b/chat/demo/entry/src/main/ets/api/SystemApi.ets new file mode 100644 index 0000000..6312861 --- /dev/null +++ b/chat/demo/entry/src/main/ets/api/SystemApi.ets @@ -0,0 +1,21 @@ +import { VersionBean } from '../bean/VersionBean'; +import { http } from '../network'; + +/** 与 Android `SystemApi.versionLatest` 使用相同的 appId 查询参数 */ +const VERSION_APP_ID = 'com.zlxt.histar'; + +/** `GET /api/v1/version/latest` 查询参数 */ +interface VersionLatestQuery { + appId: string; +} + +/** + * 获取服务端最新版本信息。 + * 对齐 Android:`GET /api/v1/version/latest?appId=...` + */ +export const getVersionLatest = (): Promise => { + const query: VersionLatestQuery = { + appId: VERSION_APP_ID, + }; + return http.get('/api/v1/version/latest', query); +}; diff --git a/chat/demo/entry/src/main/ets/api/UserApi.ets b/chat/demo/entry/src/main/ets/api/UserApi.ets new file mode 100644 index 0000000..5d0ac70 --- /dev/null +++ b/chat/demo/entry/src/main/ets/api/UserApi.ets @@ -0,0 +1,15 @@ +import { UserBean } from '../bean/UserBean'; +import { http } from '../network'; + +/** 获取当前用户资料(Http 拦截器已解包为 `data` 字段)。 */ +export const getUserInfo = (): Promise => { + return http.get('/api/v1/user/profile'); +}; + +/** + * 注销当前账号(对齐 Android `UserApi.cancelAccount` → DELETE `/api/v1/user/cancelAccount`)。 + * @returns 服务端返回的业务数据(一般为空字符串或提示文案) + */ +export const cancelAccount = (): Promise => { + return http.delete('/api/v1/user/cancelAccount'); +}; diff --git a/chat/demo/entry/src/main/ets/bean/LoginUserInfo.ets b/chat/demo/entry/src/main/ets/bean/LoginUserInfo.ets new file mode 100644 index 0000000..d8cf1dc --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/LoginUserInfo.ets @@ -0,0 +1,221 @@ +import { MMKV } from '@tencent/mmkv'; +import { UserBean, emptyUserBean, assignProfile, assignAll } from './UserBean'; + +/** + * 登录态单例:内部只持有一份 {@link UserBean},读写 MMKV 与业务 API 共用同一结构。 + */ +const PER_USER_MODEL = 'per_user_model'; + +export class LoginUserInfo { + private static sInstance: LoginUserInfo | null = null; + private readonly bean: UserBean = emptyUserBean(); + + private constructor() { + this.loadFromDisk(); + } + + static getInstance(): LoginUserInfo { + if (LoginUserInfo.sInstance === null) { + LoginUserInfo.sInstance = new LoginUserInfo(); + } + return LoginUserInfo.sInstance; + } + + /** 只读访问完整数据(字段与 UserBean / MMKV 一致) */ + getData(): UserBean { + return this.bean; + } + + getToken(): string { + return this.bean.token; + } + + getRefreshToken(): string { + return this.bean.refresh_token; + } + + getExpiresIn(): number { + return this.bean.expires_in; + } + + getUserId(): string { + return this.bean.user_id; + } + + getPhoneNumber(): string { + return this.bean.phone_number; + } + + getNickname(): string { + return this.bean.nickname; + } + + getAvatarUrl(): string { + return this.bean.avatar_url; + } + + isLogin(): boolean { + return this.bean.is_login && this.bean.token.length > 0; + } + + isExperimentTypeAi(): boolean { + return this.bean.experiment_type === 'AI'; + } + + /** {@link getUserInfo}:只更新资料段,保留已有 token */ + updateFromUserBean(source: UserBean): void { + assignProfile(this.bean, source); + this.save(); + } + + /** Flutter 登录成功:只写会话段 */ + updateFromLoginSuccess(args: Record): void { + const p = LoginUserInfo.unwrapData(args); + const tokenVal = p['token'] ?? args['token']; + const refreshVal = p['refresh_token'] ?? p['refreshToken'] ?? args['refresh_token'] ?? args['refreshToken']; + const expiresVal = p['expires_in'] ?? p['expiresIn'] ?? args['expires_in'] ?? args['expiresIn']; + + if (typeof tokenVal === 'string') { + this.bean.token = tokenVal; + } + if (typeof refreshVal === 'string') { + this.bean.refresh_token = refreshVal; + } + if (typeof expiresVal === 'number') { + this.bean.expires_in = expiresVal; + } else if (typeof expiresVal === 'string') { + const n = Number.parseInt(expiresVal, 10); + if (!Number.isNaN(n)) { + this.bean.expires_in = n; + } + } + this.bean.is_login = this.bean.token.length > 0; + this.save(); + } + + cleanUserInfo(): void { + assignAll(this.bean, emptyUserBean()); + this.save(); + } + + debugDumpForTest(): string { + const b = this.bean; + return [ + '=== LoginUserInfo (UserBean) ===', + `is_login: ${b.is_login}, isLogin(): ${this.isLogin()}`, + `token: ${b.token}`, + `refresh_token: ${b.refresh_token}`, + `expires_in: ${b.expires_in}`, + `user_id: ${b.user_id}`, + `phone_number: ${b.phone_number}`, + `nickname: ${b.nickname}`, + `avatar_url: ${b.avatar_url}`, + `imId: ${b.imId}`, + `experiment_type: ${b.experiment_type}`, + '==============================', + ].join('\n'); + } + + private getMMKV(): MMKV | null { + try { + return MMKV.defaultMMKV(); + } catch (_) { + return null; + } + } + + private loadFromDisk(): void { + const mmkv = this.getMMKV(); + if (mmkv === null) { + return; + } + const json = mmkv.decodeString(PER_USER_MODEL); + if (json === null || json === undefined || json.length === 0) { + return; + } + try { + const raw = JSON.parse(json) as Record; + assignAll(this.bean, LoginUserInfo.parseFromRecord(raw)); + } catch (err) { + console.error(`LoginUserInfo loadFromDisk failed: ${JSON.stringify(err)}`); + } + } + + private save(): void { + const mmkv = this.getMMKV(); + if (mmkv === null) { + return; + } + try { + mmkv.encodeString(PER_USER_MODEL, JSON.stringify(this.bean)); + } catch (err) { + console.error(`LoginUserInfo save failed: ${JSON.stringify(err)}`); + } + } + + /** 从 MMKV JSON 解析为 UserBean(兼容旧版 camelCase 键名) */ + private static parseFromRecord(raw: Record): UserBean { + return { + token: LoginUserInfo.str(raw, 'token'), + refresh_token: LoginUserInfo.str(raw, 'refresh_token', 'refreshToken'), + expires_in: LoginUserInfo.num(raw, 'expires_in', 'expiresIn'), + is_login: LoginUserInfo.bool(raw, 'is_login', 'isLoginFlag'), + zone: LoginUserInfo.str(raw, 'zone'), + auto_login: LoginUserInfo.bool(raw, 'auto_login', 'autoLogin'), + user_id: LoginUserInfo.str(raw, 'user_id', 'userId'), + nickname: LoginUserInfo.str(raw, 'nickname'), + avatar_url: LoginUserInfo.str(raw, 'avatar_url', 'avatarUrl'), + phone_number: LoginUserInfo.str(raw, 'phone_number', 'phoneNumber'), + gender: LoginUserInfo.num(raw, 'gender'), + city: LoginUserInfo.str(raw, 'city'), + country: LoginUserInfo.str(raw, 'country'), + province: LoginUserInfo.str(raw, 'province'), + registration_date: LoginUserInfo.str(raw, 'registration_date', 'registrationDate'), + last_login_at: LoginUserInfo.str(raw, 'last_login_at', 'lastLoginAt'), + mark: LoginUserInfo.num(raw, 'mark'), + imId: LoginUserInfo.str(raw, 'imId'), + imUserSig: LoginUserInfo.str(raw, 'imUserSig'), + imUserSigExpire: LoginUserInfo.str(raw, 'imUserSigExpire'), + experiment_type: LoginUserInfo.str(raw, 'experiment_type', 'experimentType'), + experiment_version: LoginUserInfo.num(raw, 'experiment_version', 'experimentVersion'), + }; + } + + private static unwrapData(args: Record): Record { + const wrapped = args['data']; + if (wrapped !== undefined && wrapped !== null && typeof wrapped === 'object') { + return wrapped as Record; + } + return {} as Record; + } + + private static str(obj: Record, key: string, alt?: string): string { + const v = obj[key] ?? (alt !== undefined ? obj[alt] : undefined); + if (typeof v === 'string') { + return v; + } + if (typeof v === 'number') { + return String(v); + } + return ''; + } + + private static num(obj: Record, key: string, alt?: string): number { + const v = obj[key] ?? (alt !== undefined ? obj[alt] : undefined); + if (typeof v === 'number' && !Number.isNaN(v)) { + return v; + } + if (typeof v === 'string') { + const n = Number.parseInt(v, 10); + return Number.isNaN(n) ? 0 : n; + } + return 0; + } + + private static bool(obj: Record, key: string, alt?: string): boolean { + const v = obj[key] ?? (alt !== undefined ? obj[alt] : undefined); + return v === true; + } +} + +export default LoginUserInfo; diff --git a/chat/demo/entry/src/main/ets/bean/UserBean.ets b/chat/demo/entry/src/main/ets/bean/UserBean.ets new file mode 100644 index 0000000..fb3c3f2 --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/UserBean.ets @@ -0,0 +1,116 @@ +/** + * 用户 + 登录态统一数据模型。 + * - `getUserInfo` 的 `data` 只填充资料段;token 等由 Flutter 登录成功后写入。 + * - MMKV / LoginUserInfo 单例均使用本结构,字段名与接口 JSON 一致。 + */ +export interface UserBean { + // ---------- 登录会话(Flutter flutter_pushLoginSuccess / 本地 MMKV)---------- + + /** 访问令牌,请求头 Authorization: Bearer {token} */ + token: string; + /** 刷新令牌,用于续期 access token */ + refresh_token: string; + /** access token 有效时长(秒) */ + expires_in: number; + /** 是否已登录(token 非空且登录成功时为 true) */ + is_login: boolean; + /** 手机区号,如 +86 */ + zone: string; + /** 是否开启自动登录 */ + auto_login: boolean; + + // ---------- 用户资料(GET /api/v1/user/profile 的 data)---------- + + /** 业务用户 ID */ + user_id: string; + /** 昵称 */ + nickname: string; + /** 头像 URL */ + avatar_url: string; + /** 手机号(不含区号) */ + phone_number: string; + /** 性别:0 未知,1 男,2 女(与后端约定一致) */ + gender: number; + /** 城市 */ + city: string; + /** 国家 */ + country: string; + /** 省份 */ + province: string; + /** 注册时间(ISO 8601 字符串) */ + registration_date: string; + /** 最近登录时间(ISO 8601 字符串) */ + last_login_at: string; + /** 用户标记位(业务自定义) */ + mark: number; + /** 腾讯云 IM 用户 ID */ + imId: string; + /** 腾讯云 IM UserSig */ + imUserSig: string; + /** UserSig 剩余有效时间(秒,接口常为字符串) */ + imUserSigExpire: string; + /** 实验类型,如 AI */ + experiment_type: string; + /** 实验版本号 */ + experiment_version: number; +} + +export function emptyUserBean(): UserBean { + return { + token: '', + refresh_token: '', + expires_in: 0, + is_login: false, + zone: '', + auto_login: false, + user_id: '', + nickname: '', + avatar_url: '', + phone_number: '', + gender: 0, + city: '', + country: '', + province: '', + registration_date: '', + last_login_at: '', + mark: 0, + imId: '', + imUserSig: '', + imUserSigExpire: '', + experiment_type: '', + experiment_version: 0, + }; +} + +/** 仅合并资料段(不覆盖 token 等会话字段,供 getUserInfo 结果写入) */ +export function assignProfile(target: UserBean, source: UserBean): void { + target.user_id = source.user_id ?? ''; + target.nickname = source.nickname ?? ''; + target.avatar_url = source.avatar_url ?? ''; + target.phone_number = source.phone_number ?? ''; + target.gender = source.gender ?? 0; + target.city = source.city ?? ''; + target.country = source.country ?? ''; + target.province = source.province ?? ''; + target.registration_date = source.registration_date ?? ''; + target.last_login_at = source.last_login_at ?? ''; + target.mark = source.mark ?? 0; + target.imId = source.imId ?? ''; + target.imUserSig = source.imUserSig ?? ''; + target.imUserSigExpire = source.imUserSigExpire ?? ''; + target.experiment_type = source.experiment_type ?? ''; + target.experiment_version = source.experiment_version ?? 0; +} + +/** 整对象覆盖(MMKV 恢复 / 登出清空) */ +export function assignAll(target: UserBean, source: UserBean): void { + target.token = source.token ?? ''; + target.refresh_token = source.refresh_token ?? ''; + target.expires_in = source.expires_in ?? 0; + target.is_login = source.is_login ?? false; + target.zone = source.zone ?? ''; + target.auto_login = source.auto_login ?? false; + assignProfile(target, source); +} + +export default UserBean; diff --git a/chat/demo/entry/src/main/ets/bean/VersionBean.ets b/chat/demo/entry/src/main/ets/bean/VersionBean.ets new file mode 100644 index 0000000..0c61ad6 --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/VersionBean.ets @@ -0,0 +1,64 @@ +/** + * 版本信息,对齐 Android `com.zlxt.histar.bean.VersionBean`。 + */ +export class VersionBean { + appId: string = ''; + appName: string = ''; + title: string = ''; + contents: string = ''; + platform: string = ''; + versionName: string = ''; + versionCode: number = 0; + url: string = ''; + isSilent: boolean = false; + isForce: boolean = false; + isPublish: boolean = true; +} + +/** 从接口 JSON 解析为 VersionBean */ +export function parseVersionBean(raw: Record): VersionBean { + const bean = new VersionBean(); + bean.appId = VersionBeanParser.str(raw, 'appId'); + bean.appName = VersionBeanParser.str(raw, 'appName'); + bean.title = VersionBeanParser.str(raw, 'title'); + bean.contents = VersionBeanParser.str(raw, 'contents'); + bean.platform = VersionBeanParser.str(raw, 'platform'); + bean.versionName = VersionBeanParser.str(raw, 'versionName'); + bean.versionCode = VersionBeanParser.num(raw, 'versionCode'); + bean.url = VersionBeanParser.str(raw, 'url'); + bean.isSilent = VersionBeanParser.bool(raw, 'isSilent'); + bean.isForce = VersionBeanParser.bool(raw, 'isForce'); + bean.isPublish = VersionBeanParser.bool(raw, 'isPublish'); + return bean; +} + +namespace VersionBeanParser { + export function str(obj: Record, key: string): string { + const v = obj[key]; + if (typeof v === 'string') { + return v; + } + if (typeof v === 'number') { + return String(v); + } + return ''; + } + + export function num(obj: Record, key: string): number { + const v = obj[key]; + if (typeof v === 'number' && !Number.isNaN(v)) { + return v; + } + if (typeof v === 'string') { + const n = Number.parseInt(v, 10); + return Number.isNaN(n) ? 0 : n; + } + return 0; + } + + export function bool(obj: Record, key: string): boolean { + return obj[key] === true; + } +} + +export default VersionBean; diff --git a/chat/demo/entry/src/main/ets/bean/message/JumpActionBean.ets b/chat/demo/entry/src/main/ets/bean/message/JumpActionBean.ets new file mode 100644 index 0000000..b9e318e --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/message/JumpActionBean.ets @@ -0,0 +1,10 @@ +/** + * 消息详情「查看详情」类操作的跳转参数(对齐 Android `JumpActionBean`)。 + * 用于订单 / 行程 / 优惠券等 target 的 params 解析。 + */ +export class JumpActionBean { + /** 业务 id:订单 id、行程 bookId、优惠券 id 等 */ + id: string = ''; + /** 优惠券类型:0 公券,1 私券(字符串形式,需 parseInt) */ + couponType: string = ''; +} diff --git a/chat/demo/entry/src/main/ets/bean/message/MessageDetailActionBean.ets b/chat/demo/entry/src/main/ets/bean/message/MessageDetailActionBean.ets new file mode 100644 index 0000000..8ad7b12 --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/message/MessageDetailActionBean.ets @@ -0,0 +1,13 @@ +/** + * 消息详情底部操作按钮数据(对齐 Android `MessageDetailActionBean`)。 + */ +export class MessageDetailActionBean { + /** 按钮展示文案,如「查看详情」「咨询」 */ + name: string = ''; + /** 跳转参数,结构随 target 不同而变化 */ + params?: Record; + /** 动作目标,见 {@link MessageConstants} 中 TARGET_* */ + target: string = ''; + /** 动作类型,如 NATIVE */ + type: string = ''; +} diff --git a/chat/demo/entry/src/main/ets/bean/message/MessageDetailBean.ets b/chat/demo/entry/src/main/ets/bean/message/MessageDetailBean.ets new file mode 100644 index 0000000..4911124 --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/message/MessageDetailBean.ets @@ -0,0 +1,21 @@ +import { MessageDetailActionBean } from './MessageDetailActionBean'; + +/** + * 消息详情接口响应体(对齐 Android `MessageDetailBean`)。 + */ +export class MessageDetailBean { + /** 底部操作按钮列表,如「查看详情」「咨询」 */ + action: MessageDetailActionBean[] = []; + /** 业务类型 */ + bizType: string = ''; + /** 详情正文 */ + content: string = ''; + /** 创建时间 */ + createTime: string = ''; + /** 消息 id */ + id: string = ''; + /** 是否已读:服务端为 number,1 表示已读 */ + isRead: number = 0; + /** 标题 */ + title: string = ''; +} diff --git a/chat/demo/entry/src/main/ets/bean/message/MessageInfo.ets b/chat/demo/entry/src/main/ets/bean/message/MessageInfo.ets new file mode 100644 index 0000000..a7b87c0 --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/message/MessageInfo.ets @@ -0,0 +1,29 @@ +/** + * 消息中心列表单条数据(对齐 Android `MessageInfo`)。 + */ +export class MessageInfo { + /** 消息正文摘要 */ + content: string = ''; + /** 内容类型 */ + contentType: string = ''; + /** 创建时间,如 yyyy-MM-dd HH:mm:ss */ + createTime: string = ''; + /** 是否已删除标记 */ + delFlag: boolean = false; + /** 消息唯一 id */ + id: string = ''; + /** 是否重要消息 */ + isImportant: boolean = false; + /** 是否已读(列表未读红点依据此字段) */ + isRead: boolean = false; + /** 标题 */ + title: string = ''; + /** 消息类型 */ + type: string = ''; + /** 类型编码 */ + typeCode: string = ''; + /** 更新时间 */ + updateTime: string = ''; + /** 业务类型:COUPON / TRAVEL / ORDER,用于列表图标 */ + bizType: string = ''; +} diff --git a/chat/demo/entry/src/main/ets/bean/message/MessageResponse.ets b/chat/demo/entry/src/main/ets/bean/message/MessageResponse.ets new file mode 100644 index 0000000..20f5e3d --- /dev/null +++ b/chat/demo/entry/src/main/ets/bean/message/MessageResponse.ets @@ -0,0 +1,15 @@ +import { MessageInfo } from './MessageInfo'; + +/** + * 消息列表分页接口响应体(对齐 Android `MessageResponse`)。 + */ +export class MessageResponse { + /** 符合条件的消息总数 */ + total: number = 0; + /** 当前页码 */ + pageNum: number = 0; + /** 每页大小 */ + pageSize: number = 0; + /** 当前页消息列表 */ + list: MessageInfo[] = []; +} diff --git a/chat/demo/entry/src/main/ets/config/AppSettings.ets b/chat/demo/entry/src/main/ets/config/AppSettings.ets new file mode 100644 index 0000000..d1dcc2c --- /dev/null +++ b/chat/demo/entry/src/main/ets/config/AppSettings.ets @@ -0,0 +1,286 @@ +//鸿蒙上的本地小数据库,用 key-value 把登录信息、设置等存到手机,下次打开 App 还能读回来。 +import { MMKV } from '@tencent/mmkv'; +import BuildProfile from 'BuildProfile'; + +/** + * 鸿蒙版 AppSettings —— 与 Android `com.zlxt.histar.config.AppSettings` 对齐。 + * + * 设计要点: + * - 单例:`getInstance()` 全局访问。 + * - 默认值:来自 `BuildProfile`(由 `entry/build-profile.json5` 中各 buildMode 的 + * `arkOptions.buildProfileFields` 注入),等价于 Android `BuildConfig.xxx`。 + * - 持久化:通过 MMKV 以单条 JSON 的形式落盘(key = `app_settings`), + * 等价于 Android 中 `SharedPreferences + Gson` 的玩法。 + * - 写时即刻持久化:每个 setter 内部都会调用 `save()`。 + * + * 字段对齐: + * envType / baseUrl / h5Url / imSdkAppId / pushAppkey / agreePrivacy + */ + +/** MMKV 落盘 key */ +const APP_SETTINGS_KEY = 'app_settings'; + +/** 持久化结构(注意:用可选字段以兼容老版本数据) */ +interface AppSettingsPersistShape { + envType?: string; + baseUrl?: string; + h5Url?: string; + imSdkAppId?: number; + pushAppkey?: string; + agreePrivacy?: number; +} + +export class AppSettings { + // ============ 环境类型常量(与 Android `AppConfig` 对齐) ============ + static readonly ENV_TYPE_DEV: string = 'dev'; + static readonly ENV_TYPE_TEST: string = 'test'; + static readonly ENV_TYPE_UAT: string = 'uat'; + static readonly ENV_TYPE_RELEASE: string = 'release'; + + private static sInstance: AppSettings | null = null; + + // ============ 环境配置 ============ + private envType: string = ''; + private baseUrl: string = ''; + private h5Url: string = ''; + private imSdkAppId: number = 0; + private pushAppkey: string = ''; + + /** 是否同意隐私协议:0 未同意,1 已同意 */ + private agreePrivacy: number = 0; + + private constructor() { + // 先以 BuildProfile(编译期注入)作为默认值,再用磁盘上持久化的值覆盖。 + this.envType = AppSettings.readBuildProfileString('ENV_TYPE'); + this.baseUrl = AppSettings.readBuildProfileString('BASE_URL'); + this.h5Url = AppSettings.readBuildProfileString('H5_URL'); + this.imSdkAppId = AppSettings.readBuildProfileNumber('IM_SDK_APPID'); + this.pushAppkey = AppSettings.readBuildProfileString('PUSH_APPKEY'); + this.loadFromDisk(); + } + + /** 全局单例。线程模型为 UI 线程顺序执行,无需额外加锁。 */ + static getInstance(): AppSettings { + if (AppSettings.sInstance === null) { + AppSettings.sInstance = new AppSettings(); + } + return AppSettings.sInstance; + } + + // ===================== getter / setter ===================== + + getEnvType(): string { + return this.envType; + } + + setEnvType(envType: string): void { + this.envType = envType ?? ''; + this.save(); + } + + getBaseUrl(): string { + return this.baseUrl; + } + + setBaseUrl(baseUrl: string): void { + this.baseUrl = baseUrl ?? ''; + this.save(); + } + + getH5Url(): string { + return this.h5Url; + } + + setH5Url(h5Url: string): void { + this.h5Url = h5Url ?? ''; + this.save(); + } + + getImSdkAppId(): number { + return this.imSdkAppId; + } + + setImSdkAppId(imSdkAppId: number): void { + this.imSdkAppId = imSdkAppId; + this.save(); + } + + getPushAppkey(): string { + return this.pushAppkey; + } + + setPushAppkey(pushAppkey: string): void { + this.pushAppkey = pushAppkey ?? ''; + this.save(); + } + + getAgreePrivacy(): number { + return this.agreePrivacy; + } + + setAgreePrivacy(agreePrivacy: number): void { + this.agreePrivacy = agreePrivacy; + this.save(); + } + + /** 一次性批量更新环境配置(对齐 Android `setEnvironmentConfig`)。 */ + setEnvironmentConfig( + envType: string, + baseUrl: string, + h5Url: string, + imSdkAppId: number, + pushAppkey: string, + ): void { + this.envType = envType ?? ''; + this.baseUrl = baseUrl ?? ''; + this.h5Url = h5Url ?? ''; + this.imSdkAppId = imSdkAppId; + this.pushAppkey = pushAppkey ?? ''; + this.save(); + } + + /** 环境显示名(对齐 Android `AppConfig.getEnvDisplayName`)。 */ + getEnvDisplayName(): string { + switch (this.envType) { + case AppSettings.ENV_TYPE_DEV: + return 'Dev环境'; + case AppSettings.ENV_TYPE_TEST: + return 'Test环境'; + case AppSettings.ENV_TYPE_UAT: + return 'Uat环境'; + case AppSettings.ENV_TYPE_RELEASE: + return 'Release环境'; + default: + return this.envType; + } + } + + /** 调试快照(不要在生产日志里打 PUSH_APPKEY 这种敏感信息,仅本地排查用)。 */ + debugDump(): string { + const lines: string[] = [ + '=== AppSettings 调试快照 ===', + `envType: ${this.envType}`, + `baseUrl: ${this.baseUrl}`, + `h5Url: ${this.h5Url}`, + `imSdkAppId: ${this.imSdkAppId}`, + `pushAppkey: ${this.pushAppkey}`, + `agreePrivacy: ${this.agreePrivacy}`, + '============================', + ]; + return lines.join('\n'); + } + + // ===================== 内部:BuildProfile 读取 ===================== + + /** + * 安全读取 BuildProfile 上的字符串字段。 + * BuildProfile 中的字段要在 entry/build-profile.json5 的 `arkOptions.buildProfileFields` 中注入; + * 若由于配置缺失/旧产物未重建造成字段不存在,这里返回空串而不是抛异常,避免冷启动崩溃。 + * + * 注意 ArkTS 严格模式不允许从 `typeof BuildProfile` 直接断言成 `Record<...>`, + * 需要经由顶层 `Object` 中转才能合法做反射式属性访问。 + */ + private static readBuildProfileString(key: string): string { + const v = AppSettings.readBuildProfileField(key); + if (typeof v === 'string') { + return v; + } + if (typeof v === 'number') { + return String(v); + } + return ''; + } + + /** 同上,读取 number 类型的 BuildProfile 字段(不存在则返回 0)。 */ + private static readBuildProfileNumber(key: string): number { + const v = AppSettings.readBuildProfileField(key); + if (typeof v === 'number') { + return v; + } + if (typeof v === 'string') { + const parsed = Number.parseInt(v, 10); + return Number.isNaN(parsed) ? 0 : parsed; + } + return 0; + } + + /** 反射式读取 BuildProfile 上的任意字段,封装类型断言细节。 */ + private static readBuildProfileField(key: string): Object | null { + try { + // 双重断言:typeof BuildProfile -> Object -> Record,绕过 ArkTS 严格类型检查。 + const profileMap = BuildProfile as Object as Record; + const v = profileMap[key]; + return v ?? null; + } catch (_) { + return null; + } + } + + // ===================== 内部:MMKV 持久化 ===================== + + private getMMKV(): MMKV | null { + try { + return MMKV.defaultMMKV(); + } catch (_) { + // MMKV 未初始化(异常场景,例如未走 EntryAbility.onCreate 的单元测试) + return null; + } + } + + private loadFromDisk(): void { + const mmkv = this.getMMKV(); + if (mmkv === null) { + return; + } + const json = mmkv.decodeString(APP_SETTINGS_KEY); + if (json === null || json === undefined || json.length === 0) { + return; + } + try { + const data = JSON.parse(json) as AppSettingsPersistShape; + // 仅当持久化数据存在该字段时才覆盖默认值,便于线上动态切换环境。 + if (typeof data.envType === 'string' && data.envType.length > 0) { + this.envType = data.envType; + } + if (typeof data.baseUrl === 'string' && data.baseUrl.length > 0) { + this.baseUrl = data.baseUrl; + } + if (typeof data.h5Url === 'string' && data.h5Url.length > 0) { + this.h5Url = data.h5Url; + } + if (typeof data.imSdkAppId === 'number' && data.imSdkAppId > 0) { + this.imSdkAppId = data.imSdkAppId; + } + if (typeof data.pushAppkey === 'string' && data.pushAppkey.length > 0) { + this.pushAppkey = data.pushAppkey; + } + if (typeof data.agreePrivacy === 'number') { + this.agreePrivacy = data.agreePrivacy; + } + } catch (err) { + console.error(`AppSettings loadFromDisk parse failed: ${JSON.stringify(err)}`); + } + } + + private save(): void { + const mmkv = this.getMMKV(); + if (mmkv === null) { + return; + } + const data: AppSettingsPersistShape = { + envType: this.envType, + baseUrl: this.baseUrl, + h5Url: this.h5Url, + imSdkAppId: this.imSdkAppId, + pushAppkey: this.pushAppkey, + agreePrivacy: this.agreePrivacy, + }; + try { + mmkv.encodeString(APP_SETTINGS_KEY, JSON.stringify(data)); + } catch (err) { + console.error(`AppSettings save failed: ${JSON.stringify(err)}`); + } + } +} + +export default AppSettings; diff --git a/chat/demo/entry/src/main/ets/entryability/EntryAbility.ets b/chat/demo/entry/src/main/ets/entryability/EntryAbility.ets index 24495bb..cbbdf9d 100644 --- a/chat/demo/entry/src/main/ets/entryability/EntryAbility.ets +++ b/chat/demo/entry/src/main/ets/entryability/EntryAbility.ets @@ -1,33 +1,78 @@ -import { AbilityConstant, UIAbility, Want, Configuration, common } from '@kit.AbilityKit'; +import { AbilityConstant, UIAbility, Want, Configuration } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { window } from '@kit.ArkUI'; import { MMKV } from '@tencent/mmkv'; -import { ThemeState } from '@tencentcloud/atomicx'; -import { i18n } from '@kit.LocalizationKit'; -import { AppBuilderConfig, AppBuilderHelper } from '@tencentcloud/atomicx'; import { ContextProvider } from '@tencentcloud/atomicx' +import { + FlutterBoostDelegate, + FlutterBoostRouteOptions, + FlutterBoostSetupOptions, + FlutterBoostSetupOptionsBuilder, + FlutterBoost +} from 'flutter_boost' +import { FlutterEngine, FlutterManager } from '@ohos/flutter_ohos'; +import { GeneratedPluginRegistrant } from '@ohos/flutter_module'; +import router from '@ohos.router'; import { TUICallKit } from '@tencentcloud/tuicallkit'; + const DOMAIN = 0x0000; -export default class EntryAbility extends UIAbility { +export default class EntryAbility extends UIAbility implements FlutterBoostDelegate { + + pushNativeRoute(options: FlutterBoostRouteOptions, + onPageResult?: (pageName: string, result: Record) => void): void { + } + + pushFlutterRoute(options: FlutterBoostRouteOptions, + onPageResult?: (pageName: string, result: Record) => void): void { + router.pushUrl({ + url: 'pages/FlutterBoostHostPage', + params: { + uri: options.getPageName(), + uniqueId: options.getUniqueId() ?? '', + argsJson: JSON.stringify(options.getArguments()) + } + }).catch((err: Error) => { + hilog.error(DOMAIN, 'testTag', 'pushFlutterRoute fail: %{public}s', JSON.stringify(err)); + }); + } + + popRoute(options: FlutterBoostRouteOptions): boolean { + return false; + } + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); MMKV.initialize(this.context.getApplicationContext()); - // Initialize ContextProvider, which will automatically set the system configuration to AppStorage ContextProvider.init(this.context); + FlutterManager.getInstance().pushUIAbility(this); } - onDestroy(): void { hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + FlutterManager.getInstance().popUIAbility(this); } - onWindowStageCreate(windowStage: window.WindowStage): void { - // Main window is created, set main page for this ability + async onWindowStageCreate(windowStage: window.WindowStage): Promise { hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + FlutterManager.getInstance().pushWindowStage(this, windowStage); + + const options: FlutterBoostSetupOptions = new FlutterBoostSetupOptionsBuilder().build(); + try { + await FlutterBoost.getInstance().setup(this, this.context, (engine: FlutterEngine) => { + // 注册 FlutterBoost + webview_flutter_ohos 等插件(勿只注册 Boost) + GeneratedPluginRegistrant.registerWith(engine); + }, options); + } catch (err) { + hilog.error(DOMAIN, 'testTag', 'FlutterBoost.setup failed: %{public}s', + err instanceof Error ? err.message : JSON.stringify(err)); + } + TUICallKit.createInstance(this.context).attach(windowStage); + windowStage.loadContent('pages/SplashScreenPage', (err) => { if (err.code) { hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); @@ -38,25 +83,26 @@ export default class EntryAbility extends UIAbility { } onWindowStageDestroy(): void { - // Main window is destroyed, release UI related resources hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + + FlutterManager.getInstance().popWindowStage(this); + TUICallKit.createInstance(this.context).detach(); + } onForeground(): void { - // Ability has brought to foreground hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + FlutterBoost.getInstance().onForeground(); } onBackground(): void { - // Ability has back to background hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + FlutterBoost.getInstance().onBackground(); } - onConfigurationUpdate(newConfig: Configuration): void { hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onConfigurationUpdate'); - //Use ContextProvider to uniformly handle configuration updates ContextProvider.onConfigurationUpdate(newConfig); } } diff --git a/chat/demo/entry/src/main/ets/manager/AppVersionUtil.ets b/chat/demo/entry/src/main/ets/manager/AppVersionUtil.ets new file mode 100644 index 0000000..9225067 --- /dev/null +++ b/chat/demo/entry/src/main/ets/manager/AppVersionUtil.ets @@ -0,0 +1,56 @@ +import { bundleManager } from '@kit.AbilityKit'; + +/** 本机安装包版本信息 */ +export interface LocalAppVersion { + versionName: string; + versionCode: number; +} + +/** + * 读取当前 Harmony 应用版本(对齐 Android `AndroidUtil.getApkVersionName/Code`)。 + */ +export class AppVersionUtil { + private static cached: LocalAppVersion | null = null; + + static async getLocalVersion(): Promise { + if (AppVersionUtil.cached !== null) { + return AppVersionUtil.cached; + } + try { + const info = await bundleManager.getBundleInfoForSelf( + bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION + ); + const local: LocalAppVersion = { + versionName: info.versionName ?? '', + versionCode: info.versionCode ?? 0, + }; + AppVersionUtil.cached = local; + return local; + } catch (err) { + console.error(`AppVersionUtil getLocalVersion failed: ${JSON.stringify(err)}`); + return { versionName: '', versionCode: 0 }; + } + } + + /** 格式:`V1.1.0`,用于关于页菜单右侧展示 */ + static formatDisplayName(versionName: string): string { + if (versionName.length === 0) { + return ''; + } + return versionName.startsWith('V') || versionName.startsWith('v') + ? versionName + : `V${versionName}`; + } + + /** + * 格式:`版本 V1.1.0(Build64)-test` + * 对齐 Android `VersionInfoActivity` 主文案。 + */ + static formatDetailText(versionName: string, versionCode: number, envType: string): string { + const name = AppVersionUtil.formatDisplayName(versionName); + const env = envType.length > 0 ? envType : 'unknown'; + return `版本 ${name}(Build${versionCode})-${env}`; + } +} + +export default AppVersionUtil; diff --git a/chat/demo/entry/src/main/ets/manager/HMFlutterManager.ets b/chat/demo/entry/src/main/ets/manager/HMFlutterManager.ets new file mode 100644 index 0000000..0780c3e --- /dev/null +++ b/chat/demo/entry/src/main/ets/manager/HMFlutterManager.ets @@ -0,0 +1,191 @@ +import { FlutterBoost } from 'flutter_boost'; +import systemDateTime from '@ohos.systemDateTime'; +import { LoginUserInfo } from '../bean/LoginUserInfo'; +import { AppSettings } from '../config/AppSettings'; + +const TAG = 'HMFlutterManager'; + +/** Android `LoginUserInfo` 中没有的字段就放鸿蒙侧自有的常量里 */ +const APP_VERSION = '1.0.5'; + +/** 与 FlutterBoostPlugin.EventListener 结构兼容的回调签名 */ +export type HMFlutterEventCallback = (key: string, args: Record) => void; +/** 反注册函数,调用一次即可移除监听 */ +export type HMFlutterListenerRemover = () => void; + +/** + * 鸿蒙版 FlutterManager —— 对齐 Android `com.zlxt.histar.manager.FlutterManager` 的对外 API。 + * - 路由常量 + * - openFlutter / openBindPhonePage + * - appPushEnvironment / appPushNetworkConfig / appPushLoginInfo + * - addFlutterEventListener(避免业务侧直接拿 plugin) + * + * 初始化逻辑(FlutterBoost.setup)放在 EntryAbility 中完成,此处不再触碰。 + */ +export class HMFlutterManager { + // -------- Flutter 路由常量 -------- + static readonly FLUTTER_ROUTER_TRAVELDETAIL = '/travel/detail'; + static readonly FLUTTER_ROUTER_TRAVEL_ORDER_DETAIL = '/order/info'; + static readonly FLUTTER_ROUTER_SMSLOGIN = '/login/smsLogin'; + static readonly FLUTTER_ROUTER_ORDER = '/order'; + static readonly FLUTTER_ROUTER_ITINERARY = '/mine/itinerary'; + static readonly FLUTTER_ROUTER_FAVORITE = '/mine/favorite'; + static readonly FLUTTER_ROUTER_EDITPHONE = '/account/editPhone'; + static readonly FLUTTER_ROUTER_ACCOUNT_MANAGEMENT = '/account/management'; + static readonly FLUTTER_ROUTER_EDITNAME = '/account/editName'; + static readonly FLUTTER_ROUTER_TRAVELER = '/traveler'; + static readonly FLUTTER_ROUTER_COUPON = '/mine/coupon'; + static readonly FLUTTER_ROUTER_ORDER_DETAIL = '/order/detail'; + static readonly FLUTTER_ROUTER_REPORT = '/report'; + /** 内嵌 H5(隐私协议、用户协议等) */ + static readonly FLUTTER_ROUTER_WEB_URL = '/web/url'; + static readonly FLUTTER_ROUTER_TRAVEL_RESOURCE_DETAIL = '/travel/resourceDetail'; + static readonly MINE_COUPON_DETAIL = '/mine/coupon/detail'; + /** 绑定手机号 */ + static readonly LOGIN_SMS_BIND = '/login/smsBind'; + + // -------- Flutter 事件常量 -------- + static readonly FLUTTER_EVENT_LOGINSUCCESS = 'flutter_pushLoginSuccess'; + static readonly FLUTTER_EVENT_LOGINEXPIRED = 'flutter_pushLoginExpired'; + static readonly FLUTTER_EVENT_PUSHEVENTOBJECT = 'flutter_pushEventObject'; + static readonly FLUTTER_EVENT_PUSHEVENTPAGEINOUT = 'flutter_pushEventPageInOut'; + + // -------- openFlutter 防重复点击 -------- + private static readonly OPEN_FLUTTER_THROTTLE_MS = 800; + private static sLastOpenFlutterTime: number = 0; + private static sLastOpenFlutterRoute: string = ''; + + /** + * 打开 Flutter 页面,带 800ms 同路由防抖。 + * @param routeName Flutter 路由 + * @param args 可选参数,默认空 Map + * @param opaque 容器是否不透明,默认 true + */ + static openFlutter(routeName: string, args?: Record, opaque: boolean = true): void { + if (!routeName || routeName.length === 0) { + console.warn(`${TAG} openFlutter: empty routeName, ignored`); + return; + } + + const now = HMFlutterManager.nowMs(); + if (routeName === HMFlutterManager.sLastOpenFlutterRoute + && now - HMFlutterManager.sLastOpenFlutterTime < HMFlutterManager.OPEN_FLUTTER_THROTTLE_MS) { + console.info(`${TAG} openFlutter ignored by throttle, route=${routeName}`); + return; + } + HMFlutterManager.sLastOpenFlutterRoute = routeName; + HMFlutterManager.sLastOpenFlutterTime = now; + + const argument: Record = args ?? ({} as Record); + FlutterBoost.getInstance().open(routeName, argument, opaque); + } + + /** + * 打开绑定手机号页(need_bind_phone / 账号关联换绑场景),对应 Android `openBindPhonePage`。 + */ + static openBindPhonePage(platform: string, code: string, bindTicket: string): void { + const args: Record = { + 'platform': platform ?? '', + 'code': code ?? '', + 'bind_ticket': bindTicket ?? '', + }; + HMFlutterManager.openFlutter(HMFlutterManager.LOGIN_SMS_BIND, args); + } + + /** + * 推送运行环境配置 app_pushEnvironment(对齐 Android `appPushEnvironment`)。 + * 数据来源:{@link AppSettings}(默认值由 `BuildProfile` 注入,运行期可被持久化覆盖)。 + */ + static appPushEnvironment(): void { + const settings = AppSettings.getInstance(); + const inner: Record = { + 'env': settings.getEnvType(), + 'baseURL': settings.getBaseUrl(), + }; + const data: Record = { + 'code': 0, + 'data': inner, + }; + FlutterBoost.getInstance().sendEventToFlutter('app_pushEnvironment', data); + } + + /** + * 推送网络配置 app_pushNetworkConfig(对齐 Android `appPushNetworkConfig`)。 + * 已登录则附带 Authorization。 + */ + static appPushNetworkConfig(): void { + const header: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Source': 'android', + 'Version': APP_VERSION, + 'Timezone': 'Asia/Shanghai', + 'clientDst': '0', + }; + const token = LoginUserInfo.getInstance().getToken(); + if (token.length > 0) { + header['Authorization'] = `Bearer ${token}`; + } + + const settings = AppSettings.getInstance(); + const inner: Record = { + 'baseURL': settings.getBaseUrl(), + 'H5_URL': settings.getH5Url(), + 'header': header, + }; + const data: Record = { + 'code': 0, + 'data': inner, + }; + FlutterBoost.getInstance().sendEventToFlutter('app_pushNetworkConfig', data); + } + + /** + * 推送 Token 配置 app_pushLoginInfo(对齐 Android `appPushLoginInfo`)。 + * 数据从 {@link LoginUserInfo} 读取。 + */ + static appPushLoginInfo(): void { + const userInfo = LoginUserInfo.getInstance(); + const inner: Record = { + 'token': userInfo.getToken(), + 'refresh_token': userInfo.getRefreshToken(), + 'expires_in': userInfo.getExpiresIn(), + 'isLogin': userInfo.isLogin(), + }; + const data: Record = { + 'code': 0, + 'data': inner, + }; + FlutterBoost.getInstance().sendEventToFlutter('app_pushLoginInfo', data); + } + + /** + * 监听来自 Flutter 的事件(包了一层避免业务侧直接接触 plugin / EventListener)。 + * 返回值是反注册函数,调用一次即可移除监听。 + */ + static addFlutterEventListener(eventKey: string, callback: HMFlutterEventCallback): HMFlutterListenerRemover { + const plugin = FlutterBoost.getInstance().getPlugin(); + if (plugin === null || plugin === undefined) { + console.warn(`${TAG} addFlutterEventListener: plugin not ready, key=${eventKey}`); + return () => {}; + } + const remover = plugin.addEventListener(eventKey, { + onEvent: (key: string, args: Record) => { + callback(key, args); + } + }); + return () => { + remover.remove(); + }; + } + + private static nowMs(): number { + try { + return systemDateTime.getTime(); + } catch (_) { + return Date.now(); + } + } +} + +export default HMFlutterManager; diff --git a/chat/demo/entry/src/main/ets/manager/MessageNavigatorManager.ets b/chat/demo/entry/src/main/ets/manager/MessageNavigatorManager.ets new file mode 100644 index 0000000..8a8fd1f --- /dev/null +++ b/chat/demo/entry/src/main/ets/manager/MessageNavigatorManager.ets @@ -0,0 +1,99 @@ +import { Prompt } from '@kit.ArkUI'; +import { JumpActionBean } from '../bean/message/JumpActionBean'; +import { MessageConstants } from '../utils/MessageConstants'; +import { HMFlutterManager } from './HMFlutterManager'; + +const TAG = 'MessageNavigatorManager'; + +/** + * 消息详情「查看详情」按钮的页面跳转(对齐 Android `AppNavigatorManager` 的 Flutter 分支)。 + * 咨询类按钮不在此处理,由 MessageDetailPage 直接 Toast。 + */ +export class MessageNavigatorManager { + /** + * 根据服务端下发的 target 打开对应业务页。 + * @param target 动作目标,见 MessageConstants.TARGET_* + * @param params 原始 params 对象,内部会解析为 JumpActionBean + */ + static handleAction(target: string, params?: Record): void { + if (!target || target.length === 0) { + console.warn(`${TAG} handleAction: empty target`); + return; + } + + const jump = MessageNavigatorManager.parseJumpParams(params); + switch (target) { + case MessageConstants.TARGET_ORDER_INFO: + MessageNavigatorManager.openOrderDetail(jump?.id ?? ''); + break; + case MessageConstants.TARGET_TRAVEL_INFO: + MessageNavigatorManager.openTravelDetail(jump?.id ?? ''); + break; + case MessageConstants.TARGET_COUPON_LIST: + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_COUPON); + break; + case MessageConstants.TARGET_COUPON_INFO: + MessageNavigatorManager.openCouponDetail(jump); + break; + default: + console.warn(`${TAG} Unknown target: ${target}`); + break; + } + } + + /** 将 params Map 解析为 JumpActionBean */ + private static parseJumpParams(params?: Record): JumpActionBean | null { + if (!params) { + return null; + } + const bean = new JumpActionBean(); + const idVal = params['id']; + const typeVal = params['couponType']; + bean.id = typeof idVal === 'string' ? idVal : ''; + bean.couponType = typeof typeVal === 'string' ? typeVal : ''; + return bean; + } + + /** 打开 Flutter 行程详情 */ + private static openTravelDetail(bookId: string): void { + if (!bookId || bookId.length === 0) { + Prompt.showToast({ message: 'bookId 为空' }); + return; + } + const args: Record = { 'bookId': bookId }; + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_TRAVELDETAIL, args); + } + + /** 打开 Flutter 订单详情 */ + private static openOrderDetail(orderId: string): void { + if (!orderId || orderId.length === 0) { + Prompt.showToast({ message: 'orderId 为空' }); + return; + } + const args: Record = { 'orderId': orderId }; + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_ORDER_DETAIL, args); + } + + /** 打开 Flutter 优惠券详情 */ + private static openCouponDetail(jump: JumpActionBean | null): void { + const couponId = jump?.id ?? ''; + if (!couponId || couponId.length === 0) { + Prompt.showToast({ message: 'couponId 为空' }); + return; + } + let couponTypeCode = 0; + const rawType = jump?.couponType ?? ''; + if (rawType.length > 0) { + const parsed = parseInt(rawType, 10); + if (!Number.isNaN(parsed)) { + couponTypeCode = parsed; + } + } + const args: Record = { + 'couponId': couponId, + 'couponType': couponTypeCode, + 'fromCouponDetail': false, + }; + HMFlutterManager.openFlutter(HMFlutterManager.MINE_COUPON_DETAIL, args); + } +} diff --git a/chat/demo/entry/src/main/ets/manager/MessageReadState.ets b/chat/demo/entry/src/main/ets/manager/MessageReadState.ets new file mode 100644 index 0000000..f727973 --- /dev/null +++ b/chat/demo/entry/src/main/ets/manager/MessageReadState.ets @@ -0,0 +1,28 @@ +/** + * 详情页返回列表时的「待同步已读」暂存。 + * + * 背景:router.back + onPageShow + getParams 在部分时机下拿不到 readMsgId, + * 与 MessageCenterPage.lastOpenedMsgId 配合,保证返回后红点能及时消失。 + */ +export class MessageReadState { + /** 待同步到列表的已读消息 id(单次消费) */ + private static pendingReadMsgId: string = ''; + + /** + * 详情页返回前写入待同步 id。 + * @param msgId 当前查看的消息 id + */ + static setPendingRead(msgId: string): void { + MessageReadState.pendingReadMsgId = msgId; + } + + /** + * 列表页 onPageShow 时取出并清空,避免重复消费。 + * @returns 待同步的 msgId,无则返回空字符串 + */ + static consumePendingRead(): string { + const id = MessageReadState.pendingReadMsgId; + MessageReadState.pendingReadMsgId = ''; + return id; + } +} diff --git a/chat/demo/entry/src/main/ets/manager/SystemManager.ets b/chat/demo/entry/src/main/ets/manager/SystemManager.ets new file mode 100644 index 0000000..27a608d --- /dev/null +++ b/chat/demo/entry/src/main/ets/manager/SystemManager.ets @@ -0,0 +1,82 @@ +import { VersionBean } from '../bean/VersionBean'; +import { getVersionLatest } from '../api/SystemApi'; +import { AppVersionUtil, LocalAppVersion } from './AppVersionUtil'; + +export type VersionListener = (latest: VersionBean | null, hasNewVersion: boolean) => void; + +/** + * 系统能力管理(对齐 Android `SystemRepository` 版本相关逻辑)。 + */ +export class SystemManager { + private static sInstance: SystemManager | null = null; + + private latestVersion: VersionBean | null = null; + private localVersion: LocalAppVersion | null = null; + private hasNewVersion: boolean = false; + private readonly listeners: VersionListener[] = []; + private fetching: boolean = false; + + static getInstance(): SystemManager { + if (SystemManager.sInstance === null) { + SystemManager.sInstance = new SystemManager(); + } + return SystemManager.sInstance; + } + + getLatestVersion(): VersionBean | null { + return this.latestVersion; + } + + getHasNewVersion(): boolean { + return this.hasNewVersion; + } + + async getLocalVersion(): Promise { + if (this.localVersion !== null) { + return this.localVersion; + } + this.localVersion = await AppVersionUtil.getLocalVersion(); + return this.localVersion; + } + + /** 拉取服务端最新版本并通知监听方(对齐 `getVersionLastest` + LiveData) */ + async refreshLatestVersion(): Promise { + if (this.fetching) { + return; + } + this.fetching = true; + try { + const local = await this.getLocalVersion(); + const latest = await getVersionLatest(); + this.latestVersion = latest; + this.hasNewVersion = latest.versionCode > local.versionCode; + this.notifyListeners(); + } catch (err) { + console.error(`SystemManager refreshLatestVersion failed: ${JSON.stringify(err)}`); + this.notifyListeners(); + } finally { + this.fetching = false; + } + } + + addVersionListener(listener: VersionListener): () => void { + this.listeners.push(listener); + listener(this.latestVersion, this.hasNewVersion); + return () => { + const idx = this.listeners.indexOf(listener); + if (idx >= 0) { + this.listeners.splice(idx, 1); + } + }; + } + + private notifyListeners(): void { + const latest = this.latestVersion; + const hasNew = this.hasNewVersion; + this.listeners.forEach((listener: VersionListener) => { + listener(latest, hasNew); + }); + } +} + +export default SystemManager; diff --git a/chat/demo/entry/src/main/ets/network/Http.ets b/chat/demo/entry/src/main/ets/network/Http.ets new file mode 100644 index 0000000..b920479 --- /dev/null +++ b/chat/demo/entry/src/main/ets/network/Http.ets @@ -0,0 +1,123 @@ +/** + * 网络请求封装(基于 @ohos/axios,用法类似前端的 axios)。 + * + * 调用链简要说明: + * 1. 业务代码调用 Http.get/post → 走下面的 request 实例; + * 2. 【请求拦截器】在真正发请求前统一加 baseURL、Token、公共 Header; + * 3. 服务器返回后,【响应拦截器】解析 { code, message, data },成功时只把 data 交给业务层; + * 4. 失败时 Toast 提示,并用 Promise.reject 让 await / .catch 能接到错误。 + */ + +import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from '@ohos/axios'; +import { Prompt } from '@kit.ArkUI'; +import { AppSettings } from '../config/AppSettings'; +import { LoginUserInfo } from '../bean/LoginUserInfo'; + +/** + * 服务端统一响应外壳(业务数据在 data 里)。 + * 例如:{ "code": 0, "message": "success", "data": { ...用户资料... } } + */ +export interface ResponseType { + code: number; + message: string; + data: T; + ok: boolean; +} + +/** 环境配置(baseUrl 等),来自 AppSettings / 编译配置 */ +const appSettings = AppSettings.getInstance(); +/** 当前登录用户(token、userId 等),请求时要带给后端 */ +const loginUserInfo = LoginUserInfo.getInstance(); + +/** + * 创建 axios 实例:所有接口共用一个 baseURL 和超时时间。 + * - baseURL:如 https://test.histar.com/zlxt/app/,后面 get('/api/...') 会拼在这个后面 + * - timeout:10000 表示 10 秒没响应就按超时失败处理 + */ +const request = axios.create({ + baseURL: appSettings.getBaseUrl(), + timeout: 10000 +}); + +// ===================== 请求拦截器:发请求「之前」执行 ===================== +request.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // 已登录则把用户 id、Token 放进请求头(后端用来识别是谁在调接口) + if (loginUserInfo != null) { + if (loginUserInfo.getUserId() != null) { + config.headers.userId = loginUserInfo.getUserId(); + } + if (loginUserInfo.getToken() != null) { + config.headers.Authorization = 'Bearer ' + loginUserInfo.getToken(); + } + } + // 以下是与 Android / Flutter 对齐的公共 Header,每个接口都会自动带上 + config.headers.ContentType = 'application/json'; + config.headers.Accept = 'application/json'; + config.headers.Source = 'android'; + config.headers.Version = '1.1.0'; + config.headers.Timezone = 'Asia/Shanghai'; + config.headers.clientDst = '0'; + // 必须把 config 返回出去,axios 才会用修改后的配置继续发请求 + return config; + } +); + +// ===================== 响应拦截器:收到响应「之后」执行 ===================== +request.interceptors.response.use( + // 第一个函数:HTTP 成功(状态码 2xx)且能解析到 body 时进入 + (response: AxiosResponse) => { + // 调试用:把整包 JSON 打成字符串,避免控制台只显示 [object Object] + console.log('axios-response: ' + JSON.stringify(response.data)); + + const bizCode = Number(response.data.code); + // 你们接口成功时 code 为 0;部分老接口可能是 200,两种都视为成功 + if (bizCode === 0 || bizCode === 200) { + // 只把 data 返回给业务层,例如 getUserInfo 直接拿到 UserBean,不用再解一层 + return response.data.data; + } else { + // 业务失败:code 非 0/200,例如参数错误、未登录等 + const message = response.data.message as string; + Prompt.showToast({ message }); + // reject 后,调用方的 await getUserInfo() 会进 catch + return Promise.reject(message); + } + }, + // 第二个函数:网络错误、超时、HTTP 4xx/5xx 等会进这里(不会走上面的成功分支) + (error: AxiosError) => { + const message = error.message as string; + Prompt.showToast({ message }); + return Promise.reject(message); + } +); + +/** + * 对外暴露的 Http 类:业务里用 http.get / http.post,不要直接用上面的 request。 + * + * 泛型 T:表示「解包后 data 的类型」,例如 http.get('/api/v1/user/profile') + */ +export default class Http { + /** GET:参数会拼在 URL 查询串上,例如 ?page=1 */ + get(url: string, params?: object) { + return request.get(url, { + params + }); + } + + /** POST:请求体在 body 里,一般用来提交表单、登录等 */ + post(url: string, data?: object) { + return request.post(url, data); + } + + /** PUT:更新资源 */ + put(url: string, data?: object) { + return request.put(url, data); + } + + /** DELETE:删除资源,可选查询参数 params */ + delete(url: string, params?: object) { + return request.delete(url, { + params + }); + } +} diff --git a/chat/demo/entry/src/main/ets/network/index.ets b/chat/demo/entry/src/main/ets/network/index.ets new file mode 100644 index 0000000..a28aaee --- /dev/null +++ b/chat/demo/entry/src/main/ets/network/index.ets @@ -0,0 +1,3 @@ +import Http from './Http' + +export const http = new Http() \ No newline at end of file diff --git a/chat/demo/entry/src/main/ets/pages/DevLoginPage.ets b/chat/demo/entry/src/main/ets/pages/DevLoginPage.ets index a88c6ad..33cc40a 100644 --- a/chat/demo/entry/src/main/ets/pages/DevLoginPage.ets +++ b/chat/demo/entry/src/main/ets/pages/DevLoginPage.ets @@ -69,6 +69,17 @@ struct DevLoginPage { this.login(); }) + Button("跳转登录页", { stateEffect: true, type: ButtonType.Normal }) + .margin({top:20}) + .onClick(() => { + router.pushUrl({ url: "pages/LoginPage" }); + }) + Button("跳转主页", { stateEffect: true, type: ButtonType.Normal }) + .margin({top:20}) + .onClick(() => { + router.pushUrl({ url: "pages/main/MainPage" }); + }) + } .padding(16) .width('100%') diff --git a/chat/demo/entry/src/main/ets/pages/FlutterBoostHostPage.ets b/chat/demo/entry/src/main/ets/pages/FlutterBoostHostPage.ets new file mode 100644 index 0000000..53b1179 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/FlutterBoostHostPage.ets @@ -0,0 +1,135 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import router from '@ohos.router'; +import { FlutterBoostEntry } from 'flutter_boost'; +import { FlutterPage, FlutterView } from '@ohos/flutter_ohos'; +import { WindowImmersiveHelper } from '../utils/WindowImmersiveHelper'; + +const TAG = 'FlutterBoostHostPage'; +const FLUTTER_DEST_NAME = 'flutter_boost_inner'; + +export class FlutterBoostRouterOptions { + uri: string = ''; + params: Record = {}; + uniqueId: string | null = null; + + constructor(uri: string, params: Record, uniqueId: string | null) { + this.uri = uri; + this.params = params; + this.uniqueId = uniqueId; + } +} + +@Component +struct FlutterBoostInnerPage { + private flutterEntry: FlutterBoostEntry | null = null; + private flutterView: FlutterView | null = null; + private routeStack: NavPathStack | null = null; + @State private flutterReady: boolean = false; + @Prop routerOptions: FlutterBoostRouterOptions = new FlutterBoostRouterOptions('', {}, null); + + onPopCallback(result: Record): void { + hilog.info(0x0000, TAG, 'onPopCallback'); + this.routeStack?.pop(result); + } + + async aboutToAppear(): Promise { + hilog.info(0x0000, TAG, 'Inner aboutToAppear'); + this.flutterEntry = new FlutterBoostEntry(getContext(this), this.routerOptions, false, this.onPopCallback); + await this.flutterEntry.aboutToAppear(); + this.flutterView = this.flutterEntry.getFlutterView(); + this.flutterReady = this.flutterView !== null; + } + + aboutToDisappear(): void { + hilog.info(0x0000, TAG, 'Inner aboutToDisappear'); + this.flutterEntry?.aboutToDisappear(); + } + + build() { + NavDestination() { + Stack() { + if (this.flutterReady && this.flutterView) { + FlutterPage({ viewId: this.flutterView.getId() }) + } + } + .width('100%') + .height('100%') + } + .hideTitleBar(true) + .onBackPressed(() => { + if (this.flutterEntry) { + return this.flutterEntry.onBackPressed(); + } + return false; + }) + .onShown(() => { + this.flutterEntry?.onShown(); + }) + .onHidden(() => { + this.flutterEntry?.onHidden(); + }) + .onReady((ctx: NavDestinationContext) => { + try { + this.routeStack = ctx.pathStack; + this.flutterEntry?.onReady(ctx.pathStack); + } catch (e) { + hilog.error(0x0000, TAG, `onReady error: ${JSON.stringify(e)}`); + } + }) + } +} + +@Entry +@Component +struct FlutterBoostHostPage { + private stack: NavPathStack = new NavPathStack(); + @State private routerOptions: FlutterBoostRouterOptions = + new FlutterBoostRouterOptions('', {}, null); + + aboutToAppear(): void { + void WindowImmersiveHelper.enableImmersiveStatusBar(getContext(this)); + const pr = router.getParams() as Record; + const uri = String(pr['uri'] ?? ''); + const uniqueRaw = pr['uniqueId']; + const uniqueId: string | null = + (uniqueRaw === undefined || uniqueRaw === null || String(uniqueRaw) === '') ? null : String(uniqueRaw); + let args: Record = {}; + const aj = pr['argsJson']; + if (aj !== undefined && aj !== null && String(aj).length > 0) { + try { + args = JSON.parse(String(aj)) as Record; + } catch (_) { + args = {}; + } + } + this.routerOptions = new FlutterBoostRouterOptions(uri, args, uniqueId); + + setTimeout(() => { + this.stack.pushPath({ + name: FLUTTER_DEST_NAME, + param: this.routerOptions, + onPop: () => { + router.back(); + } + }); + }, 0); + } + + @Builder + navMap(builderName: string, param: FlutterBoostRouterOptions | Record): void { + if (builderName === FLUTTER_DEST_NAME) { + FlutterBoostInnerPage({ routerOptions: param as FlutterBoostRouterOptions }); + } + } + + build() { + Navigation(this.stack) { + Column().width('100%').height('100%') + } + .navDestination(this.navMap) + .hideTitleBar(true) + .width('100%') + .height('100%') + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/LoginPage.ets b/chat/demo/entry/src/main/ets/pages/LoginPage.ets new file mode 100644 index 0000000..295e5e1 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/LoginPage.ets @@ -0,0 +1,300 @@ +/** + * 登录页(视频背景 + 底部手机号登录区)。 + * + * 术语速览: + * - @Entry:声明「页面入口」,可被路由打开。 + * - @Component:声明自定义 UI 组件(这里是整个页面 struct)。 + * - @State:需要刷新界面时改变的变量要加 @State, ArkUI 才会重新执行 build()。 + * - build():描述界面长什么样,返回一棵组件树。 + * - @Builder:把一块 UI 抽成方法,方便复用(类似小组件函数)。 + * - getContext(this):拿到当前页面所属的 Ability 上下文,用来操作窗口等系统能力。 + * - getUIContext():拿到 UI 上下文,部分组件(如自定义 Toast)需要它来弹窗。 + */ + +import router from '@ohos.router'; +import { XToastUtils } from '../utils/XToastUtils'; +import { HMFlutterManager, HMFlutterListenerRemover } from '../manager/HMFlutterManager'; +import { LoginUserInfo } from '../bean/LoginUserInfo'; +import { getUserInfo } from '../api/UserApi'; +import { WindowImmersiveHelper } from '../utils/WindowImmersiveHelper'; + +@Entry +@Component +struct LoginPage { + /** 用户协议勾选框是否勾选;变化时会触发界面刷新(因为带了 @State)。 */ + @State agreementChecked: boolean = false; + + /** + * Video 组件必须绑定的控制器:用来暂停、恢复播放等。 + * 这里是页面级私有变量,不参与界面绑定,故不加 @State。 + */ + private videoController: VideoController = new VideoController(); + + /** + * Flutter 登录成功事件的「取消订阅函数」。 + * HMFlutterManager 返回的是 () => void,调用一次就会移除监听,避免页面销毁后仍收到回调。 + */ + private loginSuccessRemover: HMFlutterListenerRemover | null = null; + + /** + * 页面即将显示时调用(生命周期)。 + * 这里开启沉浸式状态栏(视频可延伸到状态栏区域)。 + * void xxx(...):表示「故意不等待」异步结果,避免生命周期方法签名变成 async。 + */ + aboutToAppear(): void { + void WindowImmersiveHelper.enableImmersiveStatusBar(getContext(this)); + } + + /** + * 页面即将消失时调用(例如路由跳转离开本页)。 + * 暂停视频、关掉 Toast、恢复普通状态栏、移除 Flutter 监听。 + */ + aboutToDisappear(): void { + this.videoController.pause(); + XToastUtils.close(); + void WindowImmersiveHelper.restoreDefaultSystemBars(getContext(this)); + if (this.loginSuccessRemover !== null) { + this.loginSuccessRemover(); + this.loginSuccessRemover = null; + } + } + + /** + * 描述整页 UI。 + * Stack:层叠布局,先写的在下层、后写的在上层。 + * alignContent: Bottom:子组件默认对齐到底部(底部面板叠在视频上)。 + */ + build() { + Stack({ alignContent: Alignment.Bottom }) { + // 全屏循环背景视频(底层) + Video({ + src: $rawfile('welcome.mp4'), // rawfile 目录下的 mp4 + controller: this.videoController + }) + .width('100%') + .height('100%') + .objectFit(ImageFit.Cover) // 铺满裁剪,类似 CSS object-fit: cover + .autoPlay(true) + .loop(true) + .controls(false) // 不显示原生播放控件 + .muted(true) // 静音,便于自动播放策略通过 + // 触摸事件穿透给上层:否则视频会挡住底部按钮的点击 + .hitTestBehavior(HitTestMode.None) + + // 底部白色登录区(上层) + this.buildBottomPanel() + } + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + // 允许布局延伸到系统安全区(状态栏、刘海等),配合沉浸式使用 + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } + + /** + * 底部登录面板:主按钮、三方入口(示例)、协议行。 + * constraintSize(minHeight:'34%'):高度至少占屏幕 34%,与设计稿底部留白一致。 + */ + @Builder + buildBottomPanel() { + Column() { + Button($r('app.string.login_phone_button'), { type: ButtonType.Normal, stateEffect: true }) + .width('100%') + .height(48) + .fontSize(16) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.hs_color_black')) + .backgroundColor($r('app.color.hs_color_brand')) + .borderRadius(24) + .onClick(() => { + this.onPhoneLoginClick(); + }) + + Row({ space: 40 }) { + this.buildSocialButton($r('app.media.login_icon_wechat')) + this.buildSocialButton($r('app.media.login_icon_alipay')) + } + .width('100%') + .justifyContent(FlexAlign.Center) + .margin({ top: 28 }) + + this.buildAgreementRow() + } + .width('100%') + .constraintSize({ minHeight: '34%' }) + .padding({ + left: 16, + right: 16, + top: 28, + bottom: 24 + }) + .backgroundColor($r('app.color.hs_color_white')) + } + + /** 单个圆形图标按钮(微信 / 支付宝);点击弹出提示 Toast。 */ + @Builder + buildSocialButton(icon: Resource) { + Row() { + Image(icon) + .width(40) + .height(40) + .objectFit(ImageFit.Contain) + } + .width(40) + .height(40) + .borderRadius(20) + .borderColor($r('app.color.hs_color_border_gray')) + .backgroundColor($r('app.color.hs_color_white')) + .justifyContent(FlexAlign.Center) + .onClick(() => { + XToastUtils.toast($r('app.string.login_third_party_coming_soon'), this.getUIContext()); + }) + } + + /** + * 协议勾选 + 富文本说明。 + * Text() { Span... }:一段文字里多种颜色/样式用 Span 拼接。 + * layoutWeight(1):在 Row 里占满剩余横向空间。 + */ + @Builder + buildAgreementRow() { + Row() { + Image(this.agreementChecked ? $r('app.media.login_checkbox_checked') : + $r('app.media.login_checkbox_unchecked')) + .width(16) + .height(16) + .margin({ top: 2 }) + .onClick(() => { + this.agreementChecked = !this.agreementChecked; + }) + + Text() { + Span($r('app.string.login_agreement_prefix')) + .fontSize(12) + .fontColor($r('app.color.hs_color_text_secondary')) + Span($r('app.string.login_user_agreement')) + .fontSize(12) + .fontColor($r('app.color.hs_color_brand_link')) + .onClick(() => { + this.openAgreementWeb( + 'https://static.histar.com/static/histar-wechat-mp/html/agreements/service.html', + '用户服务协议' + ); + }) + Span($r('app.string.login_agreement_and')) + .fontSize(12) + .fontColor($r('app.color.hs_color_text_secondary')) + Span($r('app.string.login_privacy_policy')) + .fontSize(12) + .fontColor($r('app.color.hs_color_brand_link')) + .onClick(() => { + this.openAgreementWeb( + 'https://static.histar.com/static/histar-wechat-mp/html/agreements/privacy.html', + '隐私协议' + ); + }) + Span($r('app.string.login_agreement_and')) + .fontSize(12) + .fontColor($r('app.color.hs_color_text_secondary')) + Span($r('app.string.login_operator_terms')) + .fontSize(12) + .fontColor($r('app.color.hs_color_brand_link')) + .onClick(() => { + this.openAgreementWeb( + 'https://static.histar.com/static/histar-wechat-mp/html/agreements/operator-terms.html', + '运营商认证服务条款' + ); + }) + } + .lineHeight(16) + .layoutWeight(1) + .margin({ left: 8 }) + } + .alignItems(VerticalAlign.Top) + .width('100%') + .margin({ top: 20 }) + } + + /** 打开协议 H5 页(对齐 Android LoginActivity.setupAgreementText → FlutterManager.openFlutter("/web/url"))。 */ + private openAgreementWeb(url: string, title: string): void { + const args: Record = { + 'url': url, + 'title': title, + }; + HMFlutterManager.openFlutter('/web/url', args); + } + + /** + * 点击「手机号登录/注册」: + * 1)校验协议; + * 2)向 Flutter 推送环境与网络配置(与 SettingsPage 测试逻辑一致); + * 3)监听 Flutter 登录成功事件; + * 4)打开 Flutter 短信登录页。 + */ + private onPhoneLoginClick(): void { + if (!this.agreementChecked) { + XToastUtils.toast($r('app.string.login_agreement_required'), this.getUIContext()); + return; + } + + HMFlutterManager.appPushEnvironment(); + HMFlutterManager.appPushNetworkConfig(); + + // 防止重复注册监听:先拆掉旧的再注册新的 + if (this.loginSuccessRemover !== null) { + this.loginSuccessRemover(); + this.loginSuccessRemover = null; + } + this.loginSuccessRemover = HMFlutterManager.addFlutterEventListener( + HMFlutterManager.FLUTTER_EVENT_LOGINSUCCESS, + (_key: string, args: Record) => { + // void:表示「发起异步流程但不在这里 await」,避免回调必须是 async + void this.handleFlutterLoginSuccess(args); + } + ); + + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_SMSLOGIN); + } + + /** + * Flutter 侧登录成功后的处理(异步)。 + * - 写入 LoginUserInfo(token 等,落盘到 MMKV,下次冷启动自动恢复); + * - 再推一次 app_pushLoginInfo 给 Flutter; + * - 移除监听; + * - 先恢复普通状态栏(避免下一页继承沉浸式窗口),再 replaceUrl 进 MainPage。 + * + * router.replaceUrl:用新页面替换当前路由栈(通常不再返回登录页)。 + */ + private handleFlutterLoginSuccess(args: Record): void { + console.info(`LoginPage flutter_pushLoginSuccess args=${JSON.stringify(args)}`); + LoginUserInfo.getInstance().updateFromLoginSuccess(args); + getUserInfo() + .then((userBean) => { + console.info('LoginPage getUserInfo: ' + JSON.stringify(userBean)); + LoginUserInfo.getInstance().updateFromUserBean(userBean); + }) + .catch((err: Object) => { + console.error(`LoginPage getUserInfo failed: ${JSON.stringify(err)}`); + }) + .finally(() => { + void this.finishLoginAfterProfile(); + }); + } + + /** 拉取资料(成功或失败)后:通知 Flutter、收尾监听并进入主页。 */ + private async finishLoginAfterProfile(): Promise { + HMFlutterManager.appPushLoginInfo(); + + if (this.loginSuccessRemover !== null) { + this.loginSuccessRemover(); + this.loginSuccessRemover = null; + } + + await WindowImmersiveHelper.restoreDefaultSystemBars(getContext(this)); + try { + await router.replaceUrl({ url: 'pages/main/MainPage' }); + } catch (err) { + console.error(`LoginPage replaceUrl MainPage failed: ${JSON.stringify(err)}`); + } + } +} diff --git a/chat/demo/entry/src/main/ets/pages/SettingsPage.ets b/chat/demo/entry/src/main/ets/pages/SettingsPage.ets index 590590e..62dbc66 100644 --- a/chat/demo/entry/src/main/ets/pages/SettingsPage.ets +++ b/chat/demo/entry/src/main/ets/pages/SettingsPage.ets @@ -1,7 +1,7 @@ import { MMKV } from '@tencent/mmkv'; import { ErrorInfo, LoginStore, UserProfile, Gender, LoginState } from '@tencentcloud/atomicxcore'; import { ImageSizeUtil, ThemeState, Log, Toast, TextUtils, ToastType, Avatar, AvatarSize , AvatarContentType,AvatarShape, AppBuilderConfig, Switch, LanguageState } from '@tencentcloud/atomicx'; - +import { FlutterBoost } from 'flutter_boost'; import Prompt from '@system.prompt'; import router from '@ohos.router'; import DemoConstants from '../utils/DemoConstants'; @@ -501,7 +501,58 @@ struct SettingItemView { SettingItem({ text: $r("app.string.demo_about_button_name") }) .onClick(() => { - Toast.info($r("app.string.demo_title"), this.getUIContext()); + // Toast.info($r("app.string.demo_title"), this.getUIContext()); + // 鸿蒙原生侧 (ArkTS) 发送事件 + let data: Record = { + "env": "test", + "baseURL": "https://test.histar.com/zlxt/app/" + }; + + let params: Record = { + "code": 0, + "data": data + }; + + // 发送事件给 Flutter + FlutterBoost.getInstance().sendEventToFlutter("app_pushEnvironment", params); + + // 构建 header 对象 + const mapheader: Record = { + "Content-Type": "application/json", + "Accept": "application/json", + "Source": "android", + "Version": "1.0.5", + "Timezone": "Asia/Shanghai", + "clientDst": "0" + }; + + // 构建主数据对象 + const map: Record = { + "baseURL": "https://test.histar.com/zlxt/app/", + "H5_URL": "https://test.histar.com/histar-h5/#", + "header": mapheader + }; + + // 构建最终数据包 + const data2: Record = { + "code": 0, + "data": map + }; + + // 发送到 Flutter + FlutterBoost.getInstance().sendEventToFlutter("app_pushNetworkConfig", data2); + + const plugin = FlutterBoost.getInstance().getPlugin(); + if (!plugin) return; + plugin.addEventListener('flutter_pushLoginSuccess', { + onEvent: (key: string, args: Record) => { + Toast.info(`${JSON.stringify(args)}`, this.getUIContext()); + } + }); + + FlutterBoost.getInstance().open('/login/smsLogin', {} as Record, true); + + }) Divider().height(0.4).color(this.themeState.colors.strokeColorPrimary).width('100%') diff --git a/chat/demo/entry/src/main/ets/pages/main/MainPage.ets b/chat/demo/entry/src/main/ets/pages/main/MainPage.ets new file mode 100644 index 0000000..c7bcbe8 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/main/MainPage.ets @@ -0,0 +1,339 @@ +/** + * 主页面:侧栏 + 主内容(路由 pages/MainPage)。 + * + * 侧栏布局对齐 Android `main_activty.xml`; + * 已接入 Flutter 跳转:优惠券、订单、行程、收藏、常用旅客(见 {@link DrawerFlutterMenu})。 + * 设置、消息中心已接入;头像等待后续补齐。 + */ + +import router from '@ohos.router'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; +import { DebugInfoPanel } from '../../widget/DebugInfoPanel'; +import { HMFlutterManager } from '../../manager/HMFlutterManager'; +import { LoginUserInfo } from '../../bean/LoginUserInfo'; + +/** 侧栏内已对接 Flutter 的菜单项(与 Android MainActivity.onMenuClick 分支一一对应) */ +enum DrawerFlutterMenu { + COUPON, + ORDER, + ITINERARY, + FAVORITE, + TRAVELER, +} + +@Entry +@Component +struct MainPage { + @State showSideBar: boolean = false; + @State statusBarInsetVp: number = 0; + @State navigationBarInsetVp: number = 0; + @State nickname: string = ''; + @State avatarUrl: string = ''; + + /** 页面被 Flutter 等盖住前侧栏是否展开,用于 onPageShow 恢复 */ + private sideBarOpenBeforeHide: boolean = false; + + aboutToAppear(): void { + void this.applyImmersiveLayout(); + this.refreshUserInfo(); + } + + onPageShow(): void { + void this.applyImmersiveLayout(); + this.refreshUserInfo(); + // 从 Flutter 等页面返回:若跳转前侧栏是展开的,则恢复 + if (this.sideBarOpenBeforeHide) { + this.showSideBar = true; + this.sideBarOpenBeforeHide = false; + } + } + + private refreshUserInfo(): void { + const user = LoginUserInfo.getInstance(); + this.nickname = user.getNickname(); + this.avatarUrl = user.getAvatarUrl(); + } + + /** 沉浸式 + 读取系统栏 inset(SDK12 用 padding,勿用 safeAreaPadding 布尔写法) */ + private async applyImmersiveLayout(): Promise { + const ctx = getContext(this); + await WindowImmersiveHelper.enableImmersiveStatusBar(ctx); + this.statusBarInsetVp = await WindowImmersiveHelper.getStatusBarInsetVp(ctx); + this.navigationBarInsetVp = await WindowImmersiveHelper.getNavigationBarInsetVp(ctx); + } + + /** 收起侧栏(点击蒙层等场景) */ + private closeSideBar(): void { + if (!this.showSideBar) { + return; + } + animateTo({ duration: 250 }, () => { + this.showSideBar = false; + }); + } + + private toggleSideBar(): void { + animateTo({ duration: 250 }, () => { + this.showSideBar = !this.showSideBar; + }); + } + + /** + * 打开侧栏内 Flutter 页面。 + * 路由常量与跳转方式对齐 Android `FlutterManager.openFlutter` + `MainActivity.onMenuClick`。 + */ + private openSettingsPage(): void { + // this.closeSideBar(); + router.pushUrl({ url: 'pages/settings/AppSettingsPage' }).catch((err: Error) => { + console.error(`open AppSettingsPage failed: ${JSON.stringify(err)}`); + }); + } + + /** + * 侧栏「消息」入口 → 消息中心页。 + * 对齐 Android 从主页进入 `MessageCenterActivity`。 + */ + private openMessageCenterPage(): void { + router.pushUrl({ url: 'pages/message/MessageCenterPage' }).catch((err: Error) => { + console.error(`open MessageCenterPage failed: ${JSON.stringify(err)}`); + }); + } + + private async openDrawerFlutterMenu(menu: DrawerFlutterMenu): Promise { + // 跳转前记录侧栏展开状态,不在此处收起,返回后由 onPageShow 恢复 + this.sideBarOpenBeforeHide = this.showSideBar; + await WindowImmersiveHelper.enableImmersiveStatusBar(getContext(this)); + switch (menu) { + case DrawerFlutterMenu.COUPON: + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_COUPON); + break; + case DrawerFlutterMenu.ORDER: + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_ORDER); + break; + case DrawerFlutterMenu.ITINERARY: + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_ITINERARY); + break; + case DrawerFlutterMenu.FAVORITE: + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_FAVORITE); + break; + case DrawerFlutterMenu.TRAVELER: + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_TRAVELER); + break; + } + } + + build() { + Column() { + SideBarContainer(SideBarContainerType.Overlay) { + this.DrawerPanelBuilder() + this.MainContentBuilder() + } + .showSideBar(this.showSideBar) + .sideBarWidth('80%') + .showControlButton(false) + .onChange((value: boolean) => { + this.showSideBar = value; + }) + .layoutWeight(1) + .width('100%') + } + .width('100%') + .height('100%') + .padding({ + top: 0,//this.statusBarInsetVp, + bottom: this.navigationBarInsetVp, + }) + } + + /** 侧栏整体:背景 #F3F3F6 + 顶栏用户信息 + 四宫格菜单(毛玻璃卡片) */ + @Builder + DrawerPanelBuilder() { + Column() { + this.DrawerHeaderBuilder() + this.DrawerMenuGridBuilder() + } + .width('100%') + .height('100%') + .padding({ + top: this.statusBarInsetVp, + }) + .backgroundColor('#E8E8EC') + } + + /** 顶栏:头像、昵称/设置文案、优惠券、消息 */ + @Builder + DrawerHeaderBuilder() { + Row() { + Image(this.avatarUrl.length > 0 ? this.avatarUrl : $r('app.media.app_icon')) + .width(44) + .height(44) + .borderRadius(22) + .objectFit(ImageFit.Cover) + .backgroundColor('#E8E8EC') + + Column() { + Text(this.nickname.length > 0 ? this.nickname : ' ') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor('#000000') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + + Row() { + Text('设置') + .fontSize(12) + .fontColor('#8D8E99') + Text('›') + .fontSize(14) + .fontColor('#8D8E99') + .margin({ left: 2, top: 1 }) + } + .margin({ top: 4 }) + .alignItems(VerticalAlign.Center) + .onClick(() => { + this.openSettingsPage(); + }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + .margin({ left: 10 }) + .justifyContent(FlexAlign.Center) + + Column() { + Image($r('app.media.left_icon_coupon')) + .width(24) + .height(24) + Text('优惠券') + .fontSize(12) + .fontColor('#555175') + .margin({ top: -2 }) + } + .margin({ right: 14 }) + .alignItems(HorizontalAlign.Center) + .onClick(() => { + void this.openDrawerFlutterMenu(DrawerFlutterMenu.COUPON); + }) + + Column() { + Image($r('app.media.remind')) + .width(24) + .height(24) + Text('消息') + .fontSize(12) + .fontColor('#555175') + } + .width(36) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .onClick(() => { + this.openMessageCenterPage(); + }) + } + .width('100%') + .padding(10) + .margin({ bottom: 8 }) + .alignItems(VerticalAlign.Center) + } + + /** + * 四宫格:订单 | 行程 | 收藏 | 常用旅客 + * 背景对齐 Android `@drawable/bg_glass`(半透明白 + 白描边 + 圆角) + */ + @Builder + DrawerMenuGridBuilder() { + Row() { + this.DrawerMenuGridItemBuilder('订单', $r('app.media.icon_order'), DrawerFlutterMenu.ORDER) + this.DrawerGradientDividerBuilder() + this.DrawerMenuGridItemBuilder('行程', $r('app.media.icon_trip'), DrawerFlutterMenu.ITINERARY) + this.DrawerGradientDividerBuilder() + this.DrawerMenuGridItemBuilder('收藏', $r('app.media.icon_collect'), DrawerFlutterMenu.FAVORITE) + this.DrawerGradientDividerBuilder() + this.DrawerMenuGridItemBuilder('常用旅客', $r('app.media.left_icon_user'), DrawerFlutterMenu.TRAVELER) + } + // .width('100%') + .margin({ left: 10, right: 10, top: 10 }) + .padding({ top: 6, bottom: 6 }) + .borderRadius(10) + .backgroundColor('#99FFFFFF') + .border({ width: 1, color: '#FFFFFF' }) + .justifyContent(FlexAlign.SpaceAround) + .alignItems(VerticalAlign.Center) + } + + @Builder + DrawerMenuGridItemBuilder(label: string, icon: Resource, menu: DrawerFlutterMenu) { + Column() { + Image(icon) + .width(26) + .height(26) + Text(label) + .fontSize(12) + .fontColor('#69667B') + .margin({ top: 2 }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .padding({ top: 6, bottom: 6 }) + .onClick(() => { + void this.openDrawerFlutterMenu(menu); + }) + } + + /** 竖向渐变分割线,对齐 Android `bg_gradient_border.xml` */ + @Builder + DrawerGradientDividerBuilder() { + Column() + .width(1) + .height(30) + .linearGradient({ + angle: 180, + colors: [['#FBFBFC', 0.0], ['#D0CEDF', 0.476], ['#FBFAFD', 1.0]] + }) + } + + @Builder + MainContentBuilder() { + Stack() { + Column() { + Row() { + Image($r('app.media.ic_menu')) + .width(24) + .height(24) + .margin(16) + .onClick(() => { + this.toggleSideBar(); + }) + Text('首页') + .fontSize(18) + .fontWeight(FontWeight.Medium) + } + .width('100%') + .height(56) + .backgroundColor('#E8E8EC') + + DebugInfoPanel() + } + .width('100%') + .height('100%') + + if (this.showSideBar) { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0,0,0,0.15)') + .animation({ duration: 250 }) + .onClick(() => { + this.closeSideBar(); + }) + } + } + .width('100%') + .height('100%') + .padding({ + top: this.statusBarInsetVp + }) + .backgroundColor('#E8E8EC') + .alignContent(Alignment.TopStart) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/message/MessageCenterPage.ets b/chat/demo/entry/src/main/ets/pages/message/MessageCenterPage.ets new file mode 100644 index 0000000..232ba27 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/message/MessageCenterPage.ets @@ -0,0 +1,502 @@ +/** + * 消息中心页(对齐 Android `MessageCenterActivity`)。 + * + * 功能:搜索、下拉刷新、上拉加载、左滑删除、点击进入详情; + * 从详情返回后同步本地已读状态(消除未读红点)。 + */ +import router from '@ohos.router'; +import { Prompt } from '@kit.ArkUI'; +import { deleteMessage, getMessageList } from '../../api/MessageApi'; +import { MessageInfo } from '../../bean/message/MessageInfo'; +import { MessageConstants } from '../../utils/MessageConstants'; +import { MessageReadState } from '../../manager/MessageReadState'; +import { MessageInfoHelper } from '../../utils/MessageInfoHelper'; +import { MessageTimeUtil } from '../../utils/MessageTimeUtil'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; + +/** 页面主背景色 */ +const PAGE_BG = '#FFFFFF'; +/** 列表区域背景色 */ +const LIST_BG = '#F5F5F7'; + +/** 路由参数:详情页返回时携带的已读消息 id */ +interface MessageCenterRouteParams { + readMsgId?: string; +} + +@Entry +@Component +struct MessageCenterPage { + // ---------- 布局 inset ---------- + /** 状态栏占位高度(vp) */ + @State statusBarInsetVp: number = 0; + /** 导航栏占位高度(vp) */ + @State navigationBarInsetVp: number = 0; + + // ---------- 搜索 ---------- + /** 已提交给接口的搜索关键字 */ + @State searchKeyword: string = ''; + /** 搜索框当前输入内容 */ + @State searchInput: string = ''; + /** 是否显示搜索框右侧清空按钮 */ + @State showClearSearch: boolean = false; + + // ---------- 列表与分页 ---------- + /** 当前展示的消息列表 */ + @State messageList: MessageInfo[] = []; + /** 下拉刷新中 */ + @State isRefreshing: boolean = false; + /** 上拉加载更多中 */ + @State isLoadingMore: boolean = false; + /** 是否已无更多数据 */ + @State noMore: boolean = false; + /** 列表是否为空(用于展示空态) */ + @State listEmpty: boolean = false; + + /** 当前请求页码,从 1 开始 */ + private currentPage: number = 1; + /** 是否正在请求列表(防重复请求) */ + private isLoading: boolean = false; + /** + * 最近一次打开详情的消息 id。 + * 详情返回时若 router 传参失败,用此 id 兜底标记已读。 + */ + private lastOpenedMsgId: string = ''; + + /** 首次进入:沉浸式布局 + 拉取第一页 */ + aboutToAppear(): void { + void this.applyImmersiveLayout(); + void this.refreshList(); + } + + /** + * 页面重新显示(含从详情返回)。 + * 延迟一帧同步已读,避免早于 router.back 传参。 + */ + onPageShow(): void { + void this.applyImmersiveLayout(); + setTimeout(() => { + this.applyReadStatusFromDetail(); + }, 0); + } + + /** + * 从详情返回后,将对应消息本地标为已读并刷新列表 UI。 + * 读取顺序:MessageReadState → router.getParams → lastOpenedMsgId + */ + private applyReadStatusFromDetail(): void { + let readMsgId = MessageReadState.consumePendingRead(); + if (readMsgId.length === 0) { + const params = router.getParams() as MessageCenterRouteParams; + readMsgId = MessageInfoHelper.normalizeId(params?.readMsgId); + } + if (readMsgId.length === 0 && this.lastOpenedMsgId.length > 0) { + readMsgId = this.lastOpenedMsgId; + } + if (readMsgId.length === 0) { + return; + } + this.lastOpenedMsgId = ''; + this.messageList = MessageInfoHelper.markAsRead(this.messageList, readMsgId); + } + + /** 沉浸式状态栏 + 读取系统栏 inset */ + private async applyImmersiveLayout(): Promise { + const ctx = getContext(this); + await WindowImmersiveHelper.enableImmersiveStatusBar(ctx); + this.statusBarInsetVp = await WindowImmersiveHelper.getStatusBarInsetVp(ctx); + this.navigationBarInsetVp = await WindowImmersiveHelper.getNavigationBarInsetVp(ctx); + } + + /** 下拉刷新:重置页码并请求第一页 */ + private async refreshList(): Promise { + this.currentPage = 1; + this.noMore = false; + await this.loadPage(true); + } + + /** 列表滑到底部:加载下一页 */ + private async loadMore(): Promise { + if (this.noMore || this.isLoading || this.isLoadingMore) { + return; + } + this.currentPage += 1; + await this.loadPage(false); + } + + /** + * 请求一页列表数据。 + * @param isRefresh true=刷新(替换列表),false=加载更多(追加) + */ + private async loadPage(isRefresh: boolean): Promise { + if (this.isLoading) { + return; + } + this.isLoading = true; + if (isRefresh) { + this.isRefreshing = true; + } else { + this.isLoadingMore = true; + } + + try { + const response = await getMessageList({ + pageNum: this.currentPage, + pageSize: MessageConstants.DEFAULT_PAGE_SIZE, + keyword: this.searchKeyword, + }); + const rawList = response?.list ?? []; + const pageList: MessageInfo[] = []; + for (let i = 0; i < rawList.length; i++) { + pageList.push(MessageInfoHelper.normalize(rawList[i])); + } + if (isRefresh) { + this.messageList = pageList; + } else { + this.messageList = this.messageList.concat(pageList); + } + this.listEmpty = this.messageList.length === 0; + if (pageList.length < MessageConstants.DEFAULT_PAGE_SIZE) { + this.noMore = true; + } + } catch (err) { + console.error(`MessageCenterPage loadPage failed: ${JSON.stringify(err)}`); + if (!isRefresh) { + this.currentPage = Math.max(1, this.currentPage - 1); + } + } finally { + this.isLoading = false; + this.isRefreshing = false; + this.isLoadingMore = false; + } + } + + /** 键盘搜索:提交关键字并刷新列表 */ + private onSearchSubmit(): void { + this.searchKeyword = this.searchInput.trim(); + void this.refreshList(); + } + + /** 清空搜索框并重新拉取全量列表 */ + private onClearSearch(): void { + this.searchInput = ''; + this.searchKeyword = ''; + this.showClearSearch = false; + void this.refreshList(); + } + + /** + * 打开消息详情页。 + * @param message 被点击的列表项 + */ + private openMessageDetail(message: MessageInfo): void { + const msgId = MessageInfoHelper.normalizeId(message.id); + if (msgId.length === 0) { + return; + } + this.lastOpenedMsgId = msgId; + router.pushUrl({ + url: 'pages/message/MessageDetailPage', + params: { msgId: msgId }, + }).catch((err: Error) => { + console.error(`open MessageDetailPage failed: ${JSON.stringify(err)}`); + }); + } + + /** 左滑删除:弹出二次确认框 */ + private confirmDeleteMessage(message: MessageInfo, index: number): void { + AlertDialog.show({ + title: '删除消息', + message: '确定要删除该条消息吗?', + autoCancel: true, + alignment: DialogAlignment.Center, + primaryButton: { + value: '取消', + action: () => { + }, + }, + secondaryButton: { + value: '确定', + fontColor: '#006EFF', + action: () => { + void this.performDeleteMessage(message, index); + }, + }, + }); + } + + /** + * 调用删除接口并从列表移除该项。 + * @param message 待删除消息 + * @param index 在 messageList 中的下标 + */ + private async performDeleteMessage(message: MessageInfo, index: number): Promise { + if (!message.id || message.id.length === 0) { + return; + } + try { + await deleteMessage(message.id); + const next = [...this.messageList]; + if (index >= 0 && index < next.length) { + next.splice(index, 1); + this.messageList = next; + this.listEmpty = this.messageList.length === 0; + } + } catch (err) { + Prompt.showToast({ message: '删除失败' }); + console.error(`deleteMessage failed: ${JSON.stringify(err)}`); + } + } + + /** + * 根据 bizType 返回列表左侧业务图标。 + * @returns 图标 Resource,无匹配类型时返回 null(不展示图标与红点) + */ + private getBizTypeIcon(bizType: string): Resource | null { + if (bizType === 'COUPON') { + return $r('app.media.left_icon_coupon'); + } + if (bizType === 'TRAVEL') { + return $r('app.media.icon_trip'); + } + if (bizType === 'ORDER') { + return $r('app.media.icon_order'); + } + return null; + } + + build() { + Column() { + this.TitleBarBuilder() + this.SearchBarBuilder() + Refresh({ refreshing: this.isRefreshing }) { + List() { + if (this.listEmpty && !this.isRefreshing) { + ListItem() { + this.EmptyStateBuilder() + } + } else { + ForEach(this.messageList, (message: MessageInfo, index: number) => { + ListItem() { + this.MessageListItemBuilder(message) + } + .swipeAction({ end: this.DeleteSwipeBuilder(() => { + this.confirmDeleteMessage(message, index); + }) }) + .onClick(() => { + this.openMessageDetail(message); + }) + }, (message: MessageInfo) => MessageInfoHelper.listItemKey(message)) + } + + if (this.noMore && this.messageList.length > 0) { + ListItem() { + this.FooterBuilder() + } + } + } + .width('100%') + .layoutWeight(1) + .padding({ left: 14, right: 14 }) + .backgroundColor(LIST_BG) + .onReachEnd(() => { + void this.loadMore(); + }) + } + .width('100%') + .layoutWeight(1) + .onRefreshing(() => { + void this.refreshList(); + }) + } + .width('100%') + .height('100%') + .padding({ + top: this.statusBarInsetVp, + bottom: this.navigationBarInsetVp, + }) + .backgroundColor(PAGE_BG) + } + + /** 顶部标题栏:返回 + 「消息中心」 */ + @Builder + TitleBarBuilder() { + Row() { + Image($r('app.media.back_icon')) + .width(24) + .height(24) + .onClick(() => { + router.back(); + }) + + Text('消息中心') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#000000') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + .width('100%') + .height(60) + .padding({ left: 12, right: 12 }) + .alignItems(VerticalAlign.Center) + .backgroundColor('#FFFFFF') + } + + /** 搜索栏:输入框 + 清空按钮 */ + @Builder + SearchBarBuilder() { + Stack({ alignContent: Alignment.End }) { + TextInput({ text: this.searchInput, placeholder: '请搜索你想要的内容' }) + .width('100%') + .height(40) + .fontSize(16) + .fontColor('#000000') + .placeholderColor('#B0B1B8') + .backgroundColor('#F3F3F6') + .borderRadius(8) + .padding({ left: 12, right: 40 }) + .onChange((value: string) => { + this.searchInput = value; + this.showClearSearch = value.length > 0; + }) + .onSubmit(() => { + this.onSearchSubmit(); + }) + + if (this.showClearSearch) { + Text('×') + .fontSize(22) + .fontColor('#8D8E99') + .width(40) + .height(40) + .textAlign(TextAlign.Center) + .onClick(() => { + this.onClearSearch(); + }) + } + } + .width('100%') + .padding({ left: 20, right: 20, top: 10, bottom: 10 }) + .backgroundColor('#FFFFFF') + } + + /** + * 单条消息卡片(对齐 Android `message_center_list_item`)。 + * @param message 列表数据项 + */ + @Builder + MessageListItemBuilder(message: MessageInfo) { + Column() { + Row() { + Row() { + if (this.getBizTypeIcon(message.bizType) !== null) { + Stack() { + Image(this.getBizTypeIcon(message.bizType)!) + .width(20) + .height(20) + // 未读红点:叠在图标右上角 + if (!MessageInfoHelper.isMessageRead(message.isRead)) { + Circle() + .width(6) + .height(6) + .fill('#FF3B30') + .position({ x: 14, y: 0 }) + } + } + .width(26) + .height(26) + .margin({ right: 3 }) + .flexShrink(0) + } + + Text(message.title) + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor('#000000') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + } + .layoutWeight(1) + .alignItems(VerticalAlign.Center) + .constraintSize({ minWidth: 0 }) + + Text(MessageTimeUtil.formatListTime(message.createTime)) + .fontSize(12) + .fontColor('#999999') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .margin({ left: 8 }) + .flexShrink(0) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Text(message.content) + .fontSize(14) + .fontColor('#666666') + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .margin({ top: 8 }) + .width('100%') + } + .width('100%') + .padding(13) + .margin({ top: 5, bottom: 5 }) + .borderRadius(8) + .backgroundColor('#FFFFFF') + .clip(true) + .shadow({ + radius: 4, + color: '#14000000', + offsetX: 0, + offsetY: 2, + }) + } + + /** 左滑露出的删除按钮区域 */ + @Builder + DeleteSwipeBuilder(onDelete: () => void) { + Row() { + Text('删除') + .fontSize(14) + .fontColor('#FFFFFF') + .textAlign(TextAlign.Center) + .width(60) + .height('100%') + .backgroundColor('#FF3B30') + .onClick(() => { + onDelete(); + }) + } + .height('100%') + .justifyContent(FlexAlign.Center) + } + + /** 列表底部:没有更多了 */ + @Builder + FooterBuilder() { + Text('—— 暂时没有更多了 ——') + .fontSize(12) + .fontColor('#B0B1B8') + .width('100%') + .textAlign(TextAlign.Center) + .padding({ top: 16, bottom: 16 }) + } + + /** 列表空态 */ + @Builder + EmptyStateBuilder() { + Column() { + Text('暂无消息') + .fontSize(14) + .fontColor('#B0B1B8') + } + .width('100%') + .height(200) + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/message/MessageDetailPage.ets b/chat/demo/entry/src/main/ets/pages/message/MessageDetailPage.ets new file mode 100644 index 0000000..21f05f4 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/message/MessageDetailPage.ets @@ -0,0 +1,303 @@ +/** + * 消息详情页(对齐 Android `MessageDetailActivity`)。 + * + * - 展示标题、时间、正文; + * - 底部操作按钮:「查看详情」→ Flutter 业务页;「咨询」→ Toast 敬请期待; + * - 返回列表时回传已读 msgId,供列表页消除红点。 + */ +import router from '@ohos.router'; +import { Prompt } from '@kit.ArkUI'; +import { getMessageDetail } from '../../api/MessageApi'; +import { MessageDetailActionBean } from '../../bean/message/MessageDetailActionBean'; +import { MessageDetailBean } from '../../bean/message/MessageDetailBean'; +import { MessageNavigatorManager } from '../../manager/MessageNavigatorManager'; +import { MessageConstants } from '../../utils/MessageConstants'; +import { MessageTimeUtil } from '../../utils/MessageTimeUtil'; +import { MessageReadState } from '../../manager/MessageReadState'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; + +/** 页面顶栏与底栏背景 */ +const PAGE_BG = '#FFFFFF'; +/** 正文滚动区背景 */ +const BODY_BG = '#F5F5F7'; + +/** 路由入参:列表页传入的 msgId */ +interface MessageDetailRouteParams { + msgId?: string; +} + +@Entry +@Component +struct MessageDetailPage { + // ---------- 布局 inset ---------- + @State statusBarInsetVp: number = 0; + @State navigationBarInsetVp: number = 0; + + // ---------- 详情展示 ---------- + /** 消息标题 */ + @State title: string = ''; + /** 格式化后的创建时间 */ + @State createTime: string = ''; + /** 消息正文 */ + @State content: string = ''; + /** 底部操作按钮列表 */ + @State actions: MessageDetailActionBean[] = []; + + // ---------- 加载状态 ---------- + /** 详情接口请求中 */ + @State isLoading: boolean = true; + /** 加载失败(如无 msgId 或接口错误) */ + @State loadFailed: boolean = false; + + /** 当前消息 id,返回列表时用于标记已读 */ + private msgId: string = ''; + + /** + * 解析路由参数并拉取详情。 + * msgId 为空时直接返回上一页。 + */ + aboutToAppear(): void { + void this.applyImmersiveLayout(); + const params = router.getParams() as MessageDetailRouteParams; + this.msgId = params?.msgId ?? ''; + if (!this.msgId || this.msgId.length === 0) { + this.loadFailed = true; + this.isLoading = false; + router.back(); + return; + } + void this.loadDetail(this.msgId); + } + + /** 沉浸式状态栏 + 读取系统栏 inset */ + private async applyImmersiveLayout(): Promise { + const ctx = getContext(this); + await WindowImmersiveHelper.enableImmersiveStatusBar(ctx); + this.statusBarInsetVp = await WindowImmersiveHelper.getStatusBarInsetVp(ctx); + this.navigationBarInsetVp = await WindowImmersiveHelper.getNavigationBarInsetVp(ctx); + } + + /** + * 请求消息详情并渲染到页面。 + * @param msgId 消息 id + */ + private async loadDetail(msgId: string): Promise { + this.isLoading = true; + this.loadFailed = false; + try { + const bean = await getMessageDetail(msgId); + this.renderDetail(bean); + } catch (err) { + console.error(`MessageDetailPage loadDetail failed: ${JSON.stringify(err)}`); + this.loadFailed = true; + router.back(); + } finally { + this.isLoading = false; + } + } + + /** 将接口数据写入 @State,驱动 UI 更新 */ + private renderDetail(bean: MessageDetailBean | null): void { + if (!bean) { + this.loadFailed = true; + return; + } + this.title = bean.title ?? ''; + this.createTime = MessageTimeUtil.formatListTime(bean.createTime ?? ''); + this.content = bean.content ?? ''; + this.actions = bean.action ?? []; + } + + /** + * 底部按钮点击。 + * 咨询类 → Toast;查看详情类 → {@link MessageNavigatorManager} + */ + private onActionClick(action: MessageDetailActionBean): void { + const target = action.target ?? ''; + const name = action.name ?? ''; + if (MessageConstants.isConsultAction(target, name)) { + Prompt.showToast({ message: '功能开发中,敬请期待' }); + return; + } + MessageNavigatorManager.handleAction(target, action.params); + } + + /** + * 带已读结果返回消息中心(对齐 Android `finishWithReadResult`)。 + * 通过 MessageReadState + router.back params 双通道通知列表页。 + */ + private finishWithReadResult(): void { + if (this.msgId.length > 0) { + MessageReadState.setPendingRead(this.msgId); + router.back({ + url: 'pages/message/MessageCenterPage', + params: { readMsgId: this.msgId }, + }); + return; + } + router.back(); + } + + build() { + Column() { + this.TitleBarBuilder() + + if (this.isLoading) { + Column() { + LoadingProgress() + .width(36) + .height(36) + .color('#006EFF') + } + .layoutWeight(1) + .width('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + } else if (!this.loadFailed) { + Scroll() { + Column() { + Column() { + Text(this.title) + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor('#131212') + .width('100%') + + Text(this.createTime) + .fontSize(12) + .fontColor('#999999') + .margin({ top: 3 }) + .width('100%') + + Text(this.content) + .fontSize(14) + .fontColor('#444444') + .lineHeight(22) + .margin({ top: 12 }) + .width('100%') + } + .width('100%') + .padding({ + left: 20, + right: 20, + top: 20, + bottom: 20, + }) + .backgroundColor('#FFFFFF') + .borderRadius(12) + .clip(true) + } + .width('100%') + .padding({ left: 16, right: 16, top: 16 }) + .alignItems(HorizontalAlign.Start) + .justifyContent(FlexAlign.Start) + } + .align(Alignment.Top) + .backgroundColor(BODY_BG) + .layoutWeight(1) + .width('100%') + .scrollBar(BarState.Off) + + if (this.actions.length > 0) { + this.ActionBarBuilder() + } + } + } + .width('100%') + .height('100%') + .padding({ + top: this.statusBarInsetVp, + bottom: this.navigationBarInsetVp, + }) + .backgroundColor(PAGE_BG) + } + + /** 顶部标题栏:返回(触发已读回传)+ 「消息详情」 */ + @Builder + TitleBarBuilder() { + Row() { + Image($r('app.media.back_icon')) + .width(24) + .height(24) + .onClick(() => { + this.finishWithReadResult(); + }) + + Text('消息详情') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#000000') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + .width('100%') + .height(60) + .padding({ left: 12, right: 12 }) + .alignItems(VerticalAlign.Center) + .backgroundColor('#FFFFFF') + } + + /** 底部按钮之间的竖向分割线(对齐 Android divider_msg_detail_action) */ + @Builder + ActionDividerBuilder() { + Column() + .width(1) + .height(24) + .backgroundColor('#E7E7E7') + .margin({ left: 3, right: 3 }) + } + + /** + * 单个底部按钮槽位:非首个按钮前插入分割线,按钮等分宽度。 + * @param action 按钮数据 + * @param index 在 actions 中的下标 + */ + @Builder + ActionSlotBuilder(action: MessageDetailActionBean, index: number) { + Row() { + if (index > 0) { + this.ActionDividerBuilder() + } + Button(action.name.length > 0 ? action.name : '操作') + .type(ButtonType.Normal) + .fontSize(16) + .fontColor('#131212') + .backgroundColor('#FFFFFF') + .borderRadius(0) + .layoutWeight(1) + .height(48) + .onClick(() => { + this.onActionClick(action); + }) + } + .layoutWeight(1) + .height(48) + .alignItems(VerticalAlign.Center) + } + + /** 底部固定操作栏:横向排列「查看详情」「咨询」等按钮 */ + @Builder + ActionBarBuilder() { + Row() { + ForEach(this.actions, (action: MessageDetailActionBean, index: number) => { + this.ActionSlotBuilder(action, index) + }, (action: MessageDetailActionBean, index: number) => `${action.target}_${index}`) + } + .alignItems(VerticalAlign.Center) + .width('100%') + .padding({ + left: 16, + right: 16, + top: 12, + bottom: 30, + }) + .backgroundColor('#FFFFFF') + .shadow({ + radius: 8, + color: '#1A000000', + offsetX: 0, + offsetY: -2, + }) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/settings/AboutPage.ets b/chat/demo/entry/src/main/ets/pages/settings/AboutPage.ets new file mode 100644 index 0000000..ad40a40 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/settings/AboutPage.ets @@ -0,0 +1,171 @@ +/** + * 关于我们(对齐 Android `AboutActivity` + `about.xml`)。 + */ +import router from '@ohos.router'; +import { HMFlutterManager } from '../../manager/HMFlutterManager'; +import { SystemManager } from '../../manager/SystemManager'; +import { AppVersionUtil } from '../../manager/AppVersionUtil'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; +import { SettingsMenuRow } from './SettingsMenuRow'; + +const PAGE_BG = '#F9F9F9'; +const PRIVACY_URL = 'https://static.histar.com/static/histar-wechat-mp/html/agreements/privacy.html'; +const SERVICE_URL = 'https://static.histar.com/static/histar-wechat-mp/html/agreements/service.html'; + +@Entry +@Component +struct AboutPage { + @State statusBarInsetVp: number = 0; + @State navigationBarInsetVp: number = 0; + @State localVersionText: string = ''; + @State hasNewVersion: boolean = false; + private versionListenerRemover: (() => void) | null = null; + + aboutToAppear(): void { + void this.applyImmersiveLayout(); + void this.loadLocalVersionText(); + this.versionListenerRemover = SystemManager.getInstance().addVersionListener( + (_latest, hasNew: boolean) => { + this.hasNewVersion = hasNew; + } + ); + } + + onPageShow(): void { + void this.applyImmersiveLayout(); + void SystemManager.getInstance().refreshLatestVersion(); + } + + /** 沉浸式 + 读取系统栏 inset(SDK12 用 padding,勿用 safeAreaPadding 布尔写法) */ + private async applyImmersiveLayout(): Promise { + const ctx = getContext(this); + await WindowImmersiveHelper.enableImmersiveStatusBar(ctx); + this.statusBarInsetVp = await WindowImmersiveHelper.getStatusBarInsetVp(ctx); + this.navigationBarInsetVp = await WindowImmersiveHelper.getNavigationBarInsetVp(ctx); + } + + aboutToDisappear(): void { + if (this.versionListenerRemover !== null) { + this.versionListenerRemover(); + this.versionListenerRemover = null; + } + } + + private async loadLocalVersionText(): Promise { + const local = await AppVersionUtil.getLocalVersion(); + this.localVersionText = AppVersionUtil.formatDisplayName(local.versionName); + } + + private openVersionInfoPage(): void { + router.pushUrl({ url: 'pages/settings/VersionInfoPage' }).catch((err: Error) => { + console.error(`open VersionInfoPage failed: ${JSON.stringify(err)}`); + }); + } + + /** 打开 Flutter 前切沉浸式,避免非沉浸式 + Flutter SafeArea 造成顶部双留白 */ + private async openFlutterRoute(route: string, args?: Record): Promise { + // await WindowImmersiveHelper.enableImmersiveStatusBar(getContext(this)); + HMFlutterManager.openFlutter(route, args); + } + + private openWebPage(url: string, title: string): void { + const args: Record = { + 'url': url, + 'title': title, + }; + void this.openFlutterRoute(HMFlutterManager.FLUTTER_ROUTER_WEB_URL, args); + } + + private openReportPage(): void { + void this.openFlutterRoute(HMFlutterManager.FLUTTER_ROUTER_REPORT); + } + + build() { + Column() { + this.TitleBarBuilder() + this.MenuCardBuilder() + Blank() + .layoutWeight(1) + Text('© 2025 HiStar. All rights reserved.') + .fontSize(12) + .fontColor('#131212') + .margin({ top: 20, bottom: 20 }) + } + .width('100%') + .height('100%') + .padding({top: this.statusBarInsetVp}) + .backgroundColor(PAGE_BG) + } + + @Builder + TitleBarBuilder() { + Row() { + Image($r('app.media.back_icon')) + .width(24) + .height(24) + .onClick(() => { + router.back(); + }) + + Text('关于我们') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#000000') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + .width('100%') + .height(65) + .padding({ left: 12, right: 12 }) + .alignItems(VerticalAlign.Center) + // .backgroundColor('#FFFFFF') + } + + @Builder + MenuCardBuilder() { + Column() { + SettingsMenuRow({ + title: '版本信息', + rightText: this.localVersionText, + showNewBadge: this.hasNewVersion, + isFirst: true, + onTap: () => { + this.openVersionInfoPage(); + }, + }) + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '隐私协议', + onTap: () => { + this.openWebPage(PRIVACY_URL, '隐私协议'); + }, + }) + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '用户协议', + onTap: () => { + this.openWebPage(SERVICE_URL, '用户服务协议'); + }, + }) + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '投诉建议', + isLast: true, + onTap: () => { + this.openReportPage(); + }, + }) + } + .width('100%') + .padding(20) + } + + @Builder + MenuDividerBuilder() { + Divider() + .strokeWidth(0.5) + .color('#F0F0F2') + .margin({ left: 20 }) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/settings/AccountCancellationPage.ets b/chat/demo/entry/src/main/ets/pages/settings/AccountCancellationPage.ets new file mode 100644 index 0000000..8542b27 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/settings/AccountCancellationPage.ets @@ -0,0 +1,301 @@ +/** + * 注销账号页(对齐 Android `AccountCancellationActivity` + `account_cancellation.xml`)。 + */ +import router from '@ohos.router'; +import { cancelAccount } from '../../api/UserApi'; +import { LoginUserInfo } from '../../bean/LoginUserInfo'; +import { HMFlutterManager } from '../../manager/HMFlutterManager'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; +import { XToastUtils } from '../../utils/XToastUtils'; + +/** 页面背景色,对齐 Android `#F9F9F9` */ +const PAGE_BG = '#F9F9F9'; +/** 警告条背景色 */ +const WARNING_BG = '#FFF0ED'; +/** 警告文字色 */ +const WARNING_TEXT_COLOR = '#F1343A'; +/** 协议卡片圆角背景色(对齐 `dialog_privacy_bg_corners` 白底) */ +const CARD_BG = '#FFFFFF'; +/** 运营商认证条款 H5 地址 */ +const OPERATOR_TERMS_URL = + 'https://static.histar.com/static/histar-wechat-mp/html/agreements/operator-terms.html'; +/** 注销成功后延迟跳转登录页(毫秒),对齐 Android Handler 1s */ +const EXIT_DELAY_MS = 1000; + +@Entry +@Component +struct AccountCancellationPage { + @State statusBarInsetVp: number = 0; + /** 是否已勾选「已阅读并同意《运营商认证服务条款》」 */ + @State agreementChecked: boolean = false; + /** 注销接口请求中,防止重复提交 */ + @State isSubmitting: boolean = false; + + aboutToAppear(): void { + void this.applyImmersiveLayout(); + } + + onPageShow(): void { + void this.applyImmersiveLayout(); + } + + /** 沉浸式 + 读取系统栏 inset */ + private async applyImmersiveLayout(): Promise { + const ctx = getContext(this); + await WindowImmersiveHelper.enableImmersiveStatusBar(ctx); + this.statusBarInsetVp = await WindowImmersiveHelper.getStatusBarInsetVp(ctx); + } + + /** + * 点击底部「确定」:未勾选协议时 Toast 提示;已勾选则弹出二次确认框。 + * 对齐 Android `tv_button_text` 点击 → `cbAgreement.isChecked()` 分支。 + */ + private onConfirmButtonClick(): void { + if (!this.agreementChecked) { + XToastUtils.toast($r('app.string.account_agreement_required'), this.getUIContext()); + return; + } + this.showCancellationConfirmDialog(); + } + + /** + * 二次确认弹窗(对齐 Android `ConfirmDialog` + `islogoff` / `logoff_tip`)。 + */ + private showCancellationConfirmDialog(): void { + AlertDialog.show({ + title: $r('app.string.account_logoff_title'), + message: $r('app.string.account_logoff_message'), + autoCancel: false, + alignment: DialogAlignment.Center, + primaryButton: { + value: $r('app.string.cancel_button'), + action: () => { + }, + }, + secondaryButton: { + value: $r('app.string.confirm'), + fontColor: '#006EFF', + action: () => { + void this.performAccountCancellation(); + }, + }, + }); + } + + /** + * 调用注销接口;成功后清空本地登录态并延迟跳转登录页。 + * 对齐 Android `UserApi.cancelAccount()` → Toast → `exitApp()`。 + */ + private async performAccountCancellation(): Promise { + if (this.isSubmitting) { + return; + } + this.isSubmitting = true; + try { + await cancelAccount(); + LoginUserInfo.getInstance().cleanUserInfo(); + XToastUtils.toast($r('app.string.account_cancel_success'), this.getUIContext()); + setTimeout(() => { + void this.exitToLoginPage(); + }, EXIT_DELAY_MS); + } catch (err) { + console.error(`AccountCancellationPage cancelAccount failed: ${JSON.stringify(err)}`); + } finally { + this.isSubmitting = false; + } + } + + /** 清空路由栈并进入 Histar 登录页(对齐 Android `mainApp.exitApp()` 回到未登录态) */ + private async exitToLoginPage(): Promise { + try { + router.clear(); + await router.replaceUrl({ url: 'pages/LoginPage' }); + } catch (err) { + console.error(`AccountCancellationPage exitToLoginPage failed: ${JSON.stringify(err)}`); + } + } + + /** 打开运营商认证服务条款 H5(对齐 Android `FlutterManager.openFlutter("/web/url")`) */ + private openOperatorTermsWeb(): void { + const args: Record = { + 'url': OPERATOR_TERMS_URL, + 'title': '运营商认证服务条款', + }; + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_WEB_URL, args); + } + + build() { + Column() { + Column() { + this.TitleBarBuilder() + } + .padding({ top: this.statusBarInsetVp }) + .backgroundColor('#FFFFFF') + + Scroll() { + Column() { + this.WarningBarBuilder() + this.ProtocolCardBuilder() + this.AgreementRowBuilder() + this.ConfirmButtonBuilder() + } + .width('100%') + } + .layoutWeight(1) + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } + .width('100%') + .height('100%') + .backgroundColor(PAGE_BG) + } + + @Builder + TitleBarBuilder() { + Row() { + Image($r('app.media.back_icon')) + .width(24) + .height(24) + .onClick(() => { + router.back(); + }) + + Text($r('app.string.account_cancel_title')) + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#000000') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + .width('100%') + .height(65) + .padding({ left: 12, right: 12 }) + .alignItems(VerticalAlign.Center) + .backgroundColor('#FFFFFF') + } + + /** 顶部红色警告条(对齐 `ll_warning`) */ + @Builder + WarningBarBuilder() { + Row() { + Stack({ alignContent: Alignment.Center }) { + Circle() + .width(20) + .height(20) + .fill(WARNING_TEXT_COLOR) + Text('!') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + } + .width(20) + .height(20) + + Text($r('app.string.account_cancellation_warning')) + .fontSize(14) + .fontColor(WARNING_TEXT_COLOR) + .layoutWeight(1) + .margin({ left: 8 }) + } + .width('100%') + .height(48) + .padding(12) + .backgroundColor(WARNING_BG) + .alignItems(VerticalAlign.Center) + } + + /** 协议说明白色卡片(对齐 `protocol_card`) */ + @Builder + ProtocolCardBuilder() { + Column() { + Text('协议说明') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor('#131212') + .margin({ bottom: 10 }) + + Text('注销后,您的账户信息将被清除,且部分权益将无法恢复,请您谨慎操作。') + .fontSize(14) + .fontColor($r('app.color.hs_color_text_secondary')) + .margin({ bottom: 20 }) + + Text('注销后将发生:') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.hs_color_text_secondary')) + .margin({ bottom: 10 }) + + Text($r('app.string.account_cancellation_content')) + .fontSize(14) + .fontColor($r('app.color.hs_color_text_secondary')) + .lineHeight(22) + + Text('温馨提示:') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.hs_color_text_secondary')) + .margin({ top: 10, bottom: 10 }) + + Text($r('app.string.account_cancellation_warm_tip')) + .fontSize(14) + .fontColor($r('app.color.hs_color_text_secondary')) + .lineHeight(22) + } + .width('100%') + .margin(10) + .padding(15) + .backgroundColor(CARD_BG) + .borderRadius(12) + } + + /** 协议勾选 + 可点击条款链接(对齐 `cb_agreement` + `tv_agreement` Spannable) */ + @Builder + AgreementRowBuilder() { + Row() { + Image(this.agreementChecked ? $r('app.media.login_checkbox_checked') : + $r('app.media.login_checkbox_unchecked')) + .width(20) + .height(20) + .onClick(() => { + this.agreementChecked = !this.agreementChecked; + }) + + Text() { + Span('已阅读并同意') + .fontSize(12) + .fontColor($r('app.color.hs_color_text_secondary')) + Span($r('app.string.login_operator_terms')) + .fontSize(12) + .fontColor($r('app.color.hs_color_brand_link')) + .onClick(() => { + this.openOperatorTermsWeb(); + }) + } + .margin({ left: 5 }) + } + .width('100%') + .justifyContent(FlexAlign.Center) + .padding({ top: 4, bottom: 4 }) + } + + /** 底部确定按钮(对齐 `tv_button_text` 品牌色圆角按钮) */ + @Builder + ConfirmButtonBuilder() { + Text($r('app.string.confirm')) + .width('100%') + .height(48) + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.hs_color_black')) + .textAlign(TextAlign.Center) + .backgroundColor($r('app.color.hs_color_brand')) + .borderRadius(24) + .margin({ left: 20, right: 20, top: 20, bottom: 20 }) + .opacity(this.isSubmitting ? 0.6 : 1) + .onClick(() => { + this.onConfirmButtonClick(); + }) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/settings/AppSettingsPage.ets b/chat/demo/entry/src/main/ets/pages/settings/AppSettingsPage.ets new file mode 100644 index 0000000..39b64c4 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/settings/AppSettingsPage.ets @@ -0,0 +1,248 @@ +/** + * 设置页(对齐 Android `SettingsActivity` + `settings.xml`)。 + */ +import router from '@ohos.router'; +import BuildProfile from 'BuildProfile'; +import { getUserInfo } from '../../api/UserApi'; +import { LoginUserInfo } from '../../bean/LoginUserInfo'; +import { AppSettings } from '../../config/AppSettings'; +import { HMFlutterManager } from '../../manager/HMFlutterManager'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; +import { SettingsMenuRow } from './SettingsMenuRow'; + +const PAGE_BG = '#F5F5F7'; + +@Entry +@Component +struct AppSettingsPage { + @State statusBarInsetVp: number = 0; + @State navigationBarInsetVp: number = 0; + @State nickname: string = ''; + @State avatarUrl: string = ''; + @State showDevelopMenu: boolean = false; + /** 清理缓存右侧文案(UI 占位,与 Android 动态计算一致的位置) */ + @State cacheSizeText: string = '—'; + + aboutToAppear(): void { + void this.applyImmersiveLayout(); + this.refreshUserInfo(); + this.showDevelopMenu = !AppSettingsPage.isReleaseBuild(); + } + + onPageShow(): void { + void this.applyImmersiveLayout(); + this.refreshUserInfo(); + void this.loadUserProfileFromServer(); + } + + private refreshUserInfo(): void { + const user = LoginUserInfo.getInstance(); + this.nickname = user.getNickname(); + this.avatarUrl = user.getAvatarUrl(); + } + + /** 对齐 Android `onResume` → `viewModel.loadUserInfo()` */ + private async loadUserProfileFromServer(): Promise { + if (!LoginUserInfo.getInstance().isLogin()) { + return; + } + try { + const userBean = await getUserInfo(); + LoginUserInfo.getInstance().updateFromUserBean(userBean); + this.refreshUserInfo(); + } catch (err) { + console.error(`AppSettingsPage loadUserProfile failed: ${JSON.stringify(err)}`); + } + } + /** 沉浸式 + 读取系统栏 inset(SDK12 用 padding,勿用 safeAreaPadding 布尔写法) */ + private async applyImmersiveLayout(): Promise { + const ctx = getContext(this); + await WindowImmersiveHelper.enableImmersiveStatusBar(ctx); + this.statusBarInsetVp = await WindowImmersiveHelper.getStatusBarInsetVp(ctx); + this.navigationBarInsetVp = await WindowImmersiveHelper.getNavigationBarInsetVp(ctx); + } + /** + * 用户信息(头像/昵称),对齐 `menu_user_header` → `FLUTTER_ROUTER_ACCOUNT_MANAGEMENT`。 + */ + private async openAccountManagementPage(): Promise { + // await WindowImmersiveHelper.enableImmersiveStatusBar(getContext(this)); + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_ACCOUNT_MANAGEMENT); + } + + /** + * 修改手机号,对齐 `menu_edit_phone` → `appPushNetworkConfig` + `FLUTTER_ROUTER_EDITPHONE`。 + */ + private async openEditPhonePage(): Promise { + // await WindowImmersiveHelper.enableImmersiveStatusBar(getContext(this)); + HMFlutterManager.appPushNetworkConfig(); + HMFlutterManager.openFlutter(HMFlutterManager.FLUTTER_ROUTER_EDITPHONE); + } + + private static isReleaseBuild(): boolean { + try { + const profileMap = BuildProfile as Object as Record; + const env = profileMap['ENV_TYPE']; + return typeof env === 'string' && env === AppSettings.ENV_TYPE_RELEASE; + } catch (_) { + return false; + } + } + + build() { + Column() { + this.TitleBarBuilder() + this.SettingsCardBuilder() + this.ExitButtonBuilder() + Blank() + .layoutWeight(1) + } + .width('100%') + .height('100%') + .padding({left: 15, right: 15, top: this.statusBarInsetVp}) + .backgroundColor(PAGE_BG) + } + + @Builder + TitleBarBuilder() { + Row() { + Image($r('app.media.back_icon')) + .width(24) + .height(24) + .onClick(() => { + router.back(); + }) + + Text('设置') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#000000') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + .width('100%') + .height(48) + .padding({ left: 12, right: 12 }) + .alignItems(VerticalAlign.Center) + } + + @Builder + SettingsCardBuilder() { + Column() { + this.UserHeaderBuilder() + this.MenuListBuilder() + } + .width('100%') + .margin({ left: 15, right: 15, top: 0 }) + .borderRadius(20) + .border({ width: 1, color: '#FFFFFF' }) + .clip(true) + .linearGradient({ + angle: 10, + colors: [['#FFFFFF', 0.0], ['#ECECF2', 1.0]], + }) + } + + @Builder + UserHeaderBuilder() { + Row() { + Image(this.avatarUrl.length > 0 ? this.avatarUrl : $r('app.media.app_icon')) + .width(50) + .height(50) + .borderRadius(25) + .objectFit(ImageFit.Cover) + .backgroundColor('#E8E8EC') + + Text(this.nickname.length > 0 ? this.nickname : ' ') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor('#000000') + .margin({ left: 10 }) + .layoutWeight(1) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + + Text('›') + .fontSize(20) + .fontColor('#8a8a8a') + .margin({ top: -1 }) + } + .width('100%') + .padding({ left: 20, right: 12, top: 20, bottom: 20 }) + .alignItems(VerticalAlign.Center) + .onClick(() => { + void this.openAccountManagementPage(); + }) + } + + @Builder + MenuListBuilder() { + Column() { + SettingsMenuRow({ + title: '修改手机号', + onTap: () => { + void this.openEditPhonePage(); + }, + }) + this.MenuDividerBuilder() + SettingsMenuRow({ title: '账号关联' }) + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '注销账号', + onTap: () => { + router.pushUrl({ url: 'pages/settings/AccountCancellationPage' }).catch((err: Error) => { + console.error(`open AccountCancellationPage failed: ${JSON.stringify(err)}`); + }); + }, + }) + if (this.showDevelopMenu) { + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '环境配置', + rightText: AppSettings.getInstance().getEnvDisplayName(), + }) + } + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '清理缓存', + rightText: this.cacheSizeText, + showArrow: false, + }) + this.MenuDividerBuilder() + SettingsMenuRow({ + title: '关于我们', + isLast: true, + onTap: () => { + router.pushUrl({ url: 'pages/settings/AboutPage' }).catch((err: Error) => { + console.error(`open AboutPage failed: ${JSON.stringify(err)}`); + }); + }, + }) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius({ bottomLeft: 20, bottomRight: 20 }) + } + + @Builder + MenuDividerBuilder() { + Divider() + .strokeWidth(0.5) + .color('#F0F0F2') + .margin({ left: 20 }) + } + + @Builder + ExitButtonBuilder() { + Text('退出') + .width('100%') + .height(48) + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.hs_color_black')) + .textAlign(TextAlign.Center) + .backgroundColor($r('app.color.hs_color_brand')) + .borderRadius(24) + .margin({ left: 20, right: 20, top: 20 }) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/settings/SettingsMenuRow.ets b/chat/demo/entry/src/main/ets/pages/settings/SettingsMenuRow.ets new file mode 100644 index 0000000..acdf688 --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/settings/SettingsMenuRow.ets @@ -0,0 +1,60 @@ +/** + * 设置页菜单行,对齐 Android `SuperTextView` + `InfoItem.Account` 样式。 + */ +@Component +export struct SettingsMenuRow { + @Prop title: string = ''; + @Prop rightText: string = ''; + @Prop showArrow: boolean = true; + @Prop showNewBadge: boolean = false; + @Prop isFirst: boolean = false; + @Prop isLast: boolean = false; + onTap?: () => void; + + build() { + Row() { + Text(this.title) + .fontSize(16) + .fontColor('#000000') + .layoutWeight(1) + + if (this.rightText.length > 0) { + Text(this.rightText) + .fontSize(14) + .fontColor('#8a8a8a') + .margin({ right: 4 }) + } + + if (this.showNewBadge) { + Circle() + .width(8) + .height(8) + .fill('#FF3B30') + .margin({ right: 6 }) + } + + if (this.showArrow) { + Text('›') + .fontSize(20) + .fontColor('#8a8a8a') + .margin({ top: -1 }) + } + } + .width('100%') + .height(52) + .padding({ left: 20, right: 12 }) + .alignItems(VerticalAlign.Center) + .backgroundColor('#FFFFFF') + .borderRadius({ + topLeft: this.isFirst ? 10 : 0, + topRight: this.isFirst ? 10 : 0, + bottomLeft: this.isLast ? 10 : 0, + bottomRight: this.isLast ? 10 : 0, + }) + .onClick(() => { + if (this.onTap !== undefined) { + this.onTap(); + } + }) + } +} diff --git a/chat/demo/entry/src/main/ets/pages/settings/VersionInfoPage.ets b/chat/demo/entry/src/main/ets/pages/settings/VersionInfoPage.ets new file mode 100644 index 0000000..f643b6a --- /dev/null +++ b/chat/demo/entry/src/main/ets/pages/settings/VersionInfoPage.ets @@ -0,0 +1,139 @@ +/** + * 版本信息页(对齐 Android `VersionInfoActivity` 展示逻辑,不实现升级下载)。 + */ +import router from '@ohos.router'; +import BuildProfile from 'BuildProfile'; +import { AppSettings } from '../../config/AppSettings'; +import { SystemManager } from '../../manager/SystemManager'; +import { AppVersionUtil } from '../../manager/AppVersionUtil'; +import { WindowImmersiveHelper } from '../../utils/WindowImmersiveHelper'; + +const PAGE_BG = '#FFFFFF'; + +@Entry +@Component +struct VersionInfoPage { + @State versionDetailText: string = ''; + @State updateHintText: string = ''; + @State updateHintColor: string = '#8a8a8a'; + + aboutToAppear(): void { + void this.loadVersionDisplay(); + } + + onPageShow(): void { + void WindowImmersiveHelper.applyAboutPageSystemBars(getContext(this)); + } + + private static readBuildEnvType(): string { + try { + const profileMap = BuildProfile as Object as Record; + const env = profileMap['ENV_TYPE']; + if (typeof env === 'string' && env.length > 0) { + return env; + } + } catch (_) { + } + return AppSettings.getInstance().getEnvType(); + } + + private async loadVersionDisplay(): Promise { + const local = await AppVersionUtil.getLocalVersion(); + const envType = VersionInfoPage.readBuildEnvType(); + this.versionDetailText = AppVersionUtil.formatDetailText( + local.versionName, + local.versionCode, + envType, + ); + + const hasNew = SystemManager.getInstance().getHasNewVersion(); + const latest = SystemManager.getInstance().getLatestVersion(); + if (hasNew && latest !== null) { + const serverName = AppVersionUtil.formatDisplayName(latest.versionName); + this.updateHintText = `发现新版本 ${serverName}`; + this.updateHintColor = '#43C78F'; + } else { + this.updateHintText = '您使用的已是最新版本'; + this.updateHintColor = '#8a8a8a'; + } + + if (latest === null) { + void SystemManager.getInstance().refreshLatestVersion().then(() => { + this.applyUpdateHintFromManager(); + }); + } + } + + private applyUpdateHintFromManager(): void { + const hasNew = SystemManager.getInstance().getHasNewVersion(); + const latest = SystemManager.getInstance().getLatestVersion(); + if (hasNew && latest !== null) { + const serverName = AppVersionUtil.formatDisplayName(latest.versionName); + this.updateHintText = `发现新版本 ${serverName}`; + this.updateHintColor = '#43C78F'; + } else { + this.updateHintText = '您使用的已是最新版本'; + this.updateHintColor = '#8a8a8a'; + } + } + + build() { + Column() { + this.TitleBarBuilder() + + Column() { + Image($r('app.media.app_icon')) + .width(100) + .height(100) + .margin({ top: 40 }) + .borderRadius(20) + + Text(this.versionDetailText) + .fontSize(16) + .fontColor('#131212') + .margin({ top: 10 }) + + Text(this.updateHintText) + .fontSize(14) + .fontColor(this.updateHintColor) + .margin({ top: 10 }) + } + .width('100%') + .layoutWeight(1) + .alignItems(HorizontalAlign.Center) + + Text('© 2025 HiStar. All rights reserved.') + .fontSize(12) + .fontColor('#131212') + .margin({ top: 20, bottom: 20 }) + } + .width('100%') + .height('100%') + .backgroundColor(PAGE_BG) + } + + @Builder + TitleBarBuilder() { + Row() { + Image($r('app.media.back_icon')) + .width(24) + .height(24) + .onClick(() => { + router.back(); + }) + + Text('版本信息') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#000000') + .layoutWeight(1) + .textAlign(TextAlign.Center) + .margin({ right: 24 }) + } + .width('100%') + .height(65) + .padding({ left: 12, right: 12 }) + .alignItems(VerticalAlign.Center) + .backgroundColor('#FFFFFF') + } +} diff --git a/chat/demo/entry/src/main/ets/signature/GenerateTestUserSig.ts b/chat/demo/entry/src/main/ets/signature/GenerateTestUserSig.ts index cbb5d2d..dd9f9fb 100644 --- a/chat/demo/entry/src/main/ets/signature/GenerateTestUserSig.ts +++ b/chat/demo/entry/src/main/ets/signature/GenerateTestUserSig.ts @@ -10,7 +10,7 @@ import pako from "pako"; * 进入腾讯云云通信[控制台](https://console.cloud.tencent.com/avc ) 创建应用,即可看到 SDKAppId, * 它是腾讯云用于区分客户的唯一标识。 */ -export const SDKAPPID = 0; +export const SDKAPPID = 1600105433; /** * 签名过期时间,建议不要设置的过短 @@ -30,7 +30,7 @@ const EXPIRETIME = 604800; * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。 * 文档:https://cloud.tencent.com/document/product/269/32688#Server */ -const SECRETKEY = ""; +const SECRETKEY = "3b2453b4925c98a752f1c9c600d232a66d9c00610671c4998080475e99742e88"; /** * 计算 UserSig 签名 diff --git a/chat/demo/entry/src/main/ets/utils/MessageConstants.ets b/chat/demo/entry/src/main/ets/utils/MessageConstants.ets new file mode 100644 index 0000000..cd6e137 --- /dev/null +++ b/chat/demo/entry/src/main/ets/utils/MessageConstants.ets @@ -0,0 +1,34 @@ +/** + * 消息模块常量(对齐 Android `Constants.MessageCenter`)。 + */ +export class MessageConstants { + /** 跳转订单详情 */ + static readonly TARGET_ORDER_INFO = 'order_info'; + /** 跳转行程详情 */ + static readonly TARGET_TRAVEL_INFO = 'travel_info'; + /** 跳转优惠券列表 */ + static readonly TARGET_COUPON_LIST = 'coupon_list'; + /** 跳转优惠券详情 */ + static readonly TARGET_COUPON_INFO = 'coupon_info'; + /** 创建会话(鸿蒙侧仅 Toast,不实现) */ + static readonly TARGET_CREATE_SESSION = 'create_session'; + /** 进入已有会话(鸿蒙侧仅 Toast,不实现) */ + static readonly TARGET_ENTER_SESSION = 'enter_session'; + + /** 列表默认每页条数(对齐 Android `PagerConfig.DEFAULT_PAGE_SIZE`) */ + static readonly DEFAULT_PAGE_SIZE = 10; + + /** + * 判断是否为「咨询」类底部按钮。 + * 鸿蒙详情页对咨询仅弹出「功能开发中」,不打开 IM 会话。 + * @param target 服务端下发的 target + * @param name 按钮文案 + */ + static isConsultAction(target: string, name: string): boolean { + if (name.indexOf('咨询') >= 0) { + return true; + } + return target === MessageConstants.TARGET_CREATE_SESSION + || target === MessageConstants.TARGET_ENTER_SESSION; + } +} diff --git a/chat/demo/entry/src/main/ets/utils/MessageInfoHelper.ets b/chat/demo/entry/src/main/ets/utils/MessageInfoHelper.ets new file mode 100644 index 0000000..bedda6f --- /dev/null +++ b/chat/demo/entry/src/main/ets/utils/MessageInfoHelper.ets @@ -0,0 +1,71 @@ +import { MessageInfo } from '../bean/message/MessageInfo'; + +/** + * 消息列表数据归一化与本地已读同步。 + * 解决接口 id 可能为 number、isRead 可能为 0/1 导致的比较与 UI 刷新问题。 + */ +export class MessageInfoHelper { + /** 将消息 id 统一转为字符串,便于与路由参数比较 */ + static normalizeId(id: string | number | undefined): string { + if (id === undefined || id === null) { + return ''; + } + return `${id}`; + } + + /** + * 判断是否已读(兼容 boolean 与 0/1)。 + * @param isRead 接口或模型中的 isRead 字段 + */ + static isMessageRead(isRead: boolean | number | undefined): boolean { + return isRead === true || isRead === 1; + } + + /** + * 将接口返回的列表项转为规范的 MessageInfo 实例。 + * @param raw 接口 JSON 反序列化后的对象 + */ + static normalize(raw: MessageInfo): MessageInfo { + const item = new MessageInfo(); + item.content = raw.content ?? ''; + item.contentType = raw.contentType ?? ''; + item.createTime = raw.createTime ?? ''; + item.delFlag = raw.delFlag === true; + item.id = MessageInfoHelper.normalizeId(raw.id); + item.isImportant = raw.isImportant === true; + item.isRead = MessageInfoHelper.isMessageRead(raw.isRead); + item.title = raw.title ?? ''; + item.type = raw.type ?? ''; + item.typeCode = raw.typeCode ?? ''; + item.updateTime = raw.updateTime ?? ''; + item.bizType = raw.bizType ?? ''; + return item; + } + + /** + * 本地将指定消息标为已读,并返回全新数组以触发 @State / ForEach 刷新。 + * @param list 当前列表 + * @param msgId 已读消息 id + */ + static markAsRead(list: MessageInfo[], msgId: string): MessageInfo[] { + const targetId = MessageInfoHelper.normalizeId(msgId); + if (targetId.length === 0) { + return list; + } + return list.map((item: MessageInfo) => { + const copy = MessageInfoHelper.normalize(item); + if (MessageInfoHelper.normalizeId(copy.id) === targetId) { + copy.isRead = true; + } + return copy; + }); + } + + /** + * 生成 ForEach 唯一 key。 + * id 相同但 isRead 变化时必须变更 key,否则列表项不会重绘、红点不消失。 + */ + static listItemKey(message: MessageInfo): string { + return `${MessageInfoHelper.normalizeId(message.id)}_${message.isRead ? 1 : 0}`; + } +} diff --git a/chat/demo/entry/src/main/ets/utils/MessageTimeUtil.ets b/chat/demo/entry/src/main/ets/utils/MessageTimeUtil.ets new file mode 100644 index 0000000..7bd0935 --- /dev/null +++ b/chat/demo/entry/src/main/ets/utils/MessageTimeUtil.ets @@ -0,0 +1,22 @@ +/** + * 消息模块时间格式化(对齐 Android `TimeUtil.yMdHmS2MdHm`)。 + */ +export class MessageTimeUtil { + /** + * 列表/详情时间展示:yyyy-MM-dd HH:mm:ss → MM-dd HH:mm。 + * @param dateTimeStr 接口原始时间字符串 + */ + static formatListTime(dateTimeStr: string): string { + if (!dateTimeStr || dateTimeStr.length === 0) { + return ''; + } + const normalized = dateTimeStr.trim(); + if (normalized.length >= 16) { + return `${normalized.substring(5, 10)} ${normalized.substring(11, 16)}`; + } + if (normalized.length >= 10) { + return normalized.substring(5, 10); + } + return normalized; + } +} diff --git a/chat/demo/entry/src/main/ets/utils/WindowImmersiveHelper.ets b/chat/demo/entry/src/main/ets/utils/WindowImmersiveHelper.ets new file mode 100644 index 0000000..dc8fc56 --- /dev/null +++ b/chat/demo/entry/src/main/ets/utils/WindowImmersiveHelper.ets @@ -0,0 +1,87 @@ +import { window } from '@kit.ArkUI'; +import { Context } from '@kit.AbilityKit'; + +/** + * 窗口沉浸式 / 标准页系统栏样式统一管理。 + * + * 约定:仅在各页 `onPageShow` 中切换窗口模式;不在 `aboutToDisappear` 里恢复, + * 避免打开 Flutter 叠层或路由返回时出现「系统栏 + SafeArea + 手动 padding」三重顶距。 + */ +export class WindowImmersiveHelper { + /** 全屏布局 + 透明状态栏(MainPage / LoginPage / Flutter 容器) */ + static async enableImmersiveStatusBar(context: Context): Promise { + try { + const win = await window.getLastWindow(context); + await win.setWindowLayoutFullScreen(true); + await win.setWindowSystemBarProperties({ + statusBarColor: '#00000000', + statusBarContentColor: '#FF131212', + navigationBarColor: '#00000000', + navigationBarContentColor: '#FF131212', + }); + } catch (_) { + } + } + + /** + * 标准非沉浸式页:由系统负责顶栏避让,页面不要再叠加 statusBarInset 类 padding。 + * @param statusBarColor 含 alpha 的 #AARRGGBB + * @param navigationBarColor 含 alpha 的 #AARRGGBB + */ + static async applyStandardPageSystemBars( + context: Context, + statusBarColor: string, + navigationBarColor: string, + ): Promise { + try { + const win = await window.getLastWindow(context); + await win.setWindowLayoutFullScreen(false); + await win.setWindowSystemBarProperties({ + statusBarColor: statusBarColor, + statusBarContentColor: '#FF000000', + navigationBarColor: navigationBarColor, + navigationBarContentColor: '#FF000000', + }); + } catch (_) { + } + } + + /** 设置页系统栏(#F5F5F7) */ + static async applySettingsPageSystemBars(context: Context): Promise { + await WindowImmersiveHelper.applyStandardPageSystemBars(context, '#FFF5F5F7', '#FFF5F5F7'); + } + + /** 关于页 / 版本信息页系统栏 */ + static async applyAboutPageSystemBars(context: Context): Promise { + await WindowImmersiveHelper.applyStandardPageSystemBars(context, '#FFFFFFFF', '#FFF9F9F9'); + } + + /** 非全屏 + 不透明白底系统栏(登录页离开等场景仍可使用) */ + static async restoreDefaultSystemBars(context: Context): Promise { + await WindowImmersiveHelper.applyStandardPageSystemBars(context, '#FFFFFFFF', '#FFFFFFFF'); + } + + /** 沉浸式全屏下,顶部状态栏占位高度(vp),用于 MainPage 等手动 padding */ + static async getStatusBarInsetVp(context: Context): Promise { + try { + const win = await window.getLastWindow(context); + const topArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); + return px2vp(topArea.topRect.height); + } catch (_) { + return 0; + } + } + + /** 沉浸式全屏下,底部导航条占位高度(vp) */ + static async getNavigationBarInsetVp(context: Context): Promise { + try { + const win = await window.getLastWindow(context); + const bottomArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); + return px2vp(bottomArea.bottomRect.height); + } catch (_) { + return 0; + } + } +} + +export default WindowImmersiveHelper; diff --git a/chat/demo/entry/src/main/ets/utils/XToastUtils.ets b/chat/demo/entry/src/main/ets/utils/XToastUtils.ets new file mode 100644 index 0000000..10ede01 --- /dev/null +++ b/chat/demo/entry/src/main/ets/utils/XToastUtils.ets @@ -0,0 +1,130 @@ +import { BusinessError } from '@kit.BasicServicesKit'; +import { ComponentContent, promptAction } from '@kit.ArkUI'; +import { UIContext } from '@ohos.arkui.UIContext'; + +/** + * 与 Android 端 XToastUtils.toast(XUI XToast.normal + Config alpha 200)视觉对齐: + * toast_normal_tint #353A3E + alpha 200 → #C8353A3E;文字 #FFFFFF。 + */ +interface XToastNormalParams { + message: string | Resource; +} + +@Builder +function buildXToastNormalContent(params: XToastNormalParams) { + Row() { + Text(params.message) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontColor('#FFFFFF') + .lineHeight(14 * 1.57) + .textAlign(TextAlign.Start) + .maxLines(10) + .textOverflow({ overflow: TextOverflow.Clip }) + } + .padding({ + left: 16, + right: 16, + top: 9, + bottom: 9 + }) + .backgroundColor('#C8353A3E') + .borderRadius(8) + .shadow({ + radius: 12, + color: '#40000000', + offsetX: 0, + offsetY: 4 + }) + .constraintSize({ + minHeight: 40, + maxWidth: 340 + }) + .justifyContent(FlexAlign.Center) +} + +export class XToastUtils { + private static uiCtx: UIContext | null = null; + private static contentNode: ComponentContent | null = null; + private static dismissTimer: number | null = null; + + /** + * 居中短时提示,默认约 2s(对齐 LENGTH_SHORT 常用时长)。 + */ + static toast(message: string | Resource, uiContext: UIContext, durationMs: number = 2000): void { + if (XToastUtils.dismissTimer !== null) { + clearTimeout(XToastUtils.dismissTimer); + XToastUtils.dismissTimer = null; + } + + XToastUtils.closeDialogSync().then(() => { + XToastUtils.uiCtx = uiContext; + const params: XToastNormalParams = { message: message }; + XToastUtils.contentNode = + new ComponentContent(uiContext, wrapBuilder(buildXToastNormalContent), params); + + const options: promptAction.BaseDialogOptions = { + alignment: DialogAlignment.Center, + autoCancel: false, + maskColor: Color.Transparent, + isModal: false + }; + + uiContext.getPromptAction() + .openCustomDialog(XToastUtils.contentNode, options) + .then(() => { + if (durationMs > 0) { + XToastUtils.dismissTimer = setTimeout(() => { + XToastUtils.close(); + }, durationMs); + } + }) + .catch((error: BusinessError) => { + console.error(`XToastUtils openCustomDialog failed: ${error.code} ${error.message}`); + }); + }); + } + + static close(): void { + if (XToastUtils.dismissTimer !== null) { + clearTimeout(XToastUtils.dismissTimer); + XToastUtils.dismissTimer = null; + } + + if (XToastUtils.contentNode !== null && XToastUtils.uiCtx !== null) { + XToastUtils.uiCtx.getPromptAction() + .closeCustomDialog(XToastUtils.contentNode) + .then(() => { + XToastUtils.contentNode = null; + }) + .catch((error: BusinessError) => { + console.error(`XToastUtils closeCustomDialog failed: ${error.code} ${error.message}`); + XToastUtils.contentNode = null; + }); + } + } + + private static closeDialogSync(): Promise { + return new Promise((resolve) => { + if (XToastUtils.dismissTimer !== null) { + clearTimeout(XToastUtils.dismissTimer); + XToastUtils.dismissTimer = null; + } + + if (XToastUtils.contentNode !== null && XToastUtils.uiCtx !== null) { + XToastUtils.uiCtx.getPromptAction() + .closeCustomDialog(XToastUtils.contentNode) + .then(() => { + XToastUtils.contentNode = null; + resolve(); + }) + .catch((_error: BusinessError) => { + XToastUtils.contentNode = null; + resolve(); + }); + } else { + resolve(); + } + }); + } +} diff --git a/chat/demo/entry/src/main/ets/widget/DebugInfoPanel.ets b/chat/demo/entry/src/main/ets/widget/DebugInfoPanel.ets new file mode 100644 index 0000000..14831bc --- /dev/null +++ b/chat/demo/entry/src/main/ets/widget/DebugInfoPanel.ets @@ -0,0 +1,96 @@ +import { LoginUserInfo } from '../bean/LoginUserInfo'; +import { AppSettings } from '../config/AppSettings'; + +const TAG = 'DebugInfoPanel'; + +/** + * 调试信息面板(仅本地排查用)。 + * + * 集中展示: + * - {@link LoginUserInfo}(登录态:token / refreshToken / userId / nickname 等) + * - {@link AppSettings}(环境配置:envType / baseUrl / h5Url / imSdkAppId / pushAppkey / agreePrivacy) + * + * 数据来源全部走单例 + MMKV,「刷新显示」会重新读取一次单例并可同步打印到控制台。 + * + * 用法(在父组件 build 内): + * ```typescript + * DebugInfoPanel() + * // 或定制: + * DebugInfoPanel({ title: '[Debug] 状态快照', logToConsoleOnInit: false }) + * ``` + * + * 注意:组件内部 Column 已使用 `layoutWeight(1)`,需要放在 *已有* 高度约束(Column / Flex 等) + * 的父容器中使用,避免高度坍缩。 + */ +@Component +export struct DebugInfoPanel { + /** 面板顶部标题,可被父组件覆盖 */ + title: string = '[调试] LoginUserInfo & AppSettings'; + /** 是否展示「刷新显示」按钮,默认开 */ + showRefreshButton: boolean = true; + /** 进入页面时是否同步打印一次到控制台(HiLog),默认开 */ + logToConsoleOnInit: boolean = true; + + /** 当前展示的 dump 文本(合并后的多行字符串) */ + @State debugText: string = ''; + + aboutToAppear(): void { + this.refresh(this.logToConsoleOnInit); + } + + build() { + Column() { + Text(this.title) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor('#131212') + .width('100%') + .padding({ left: 12, right: 12, top: 8, bottom: 4 }) + + if (this.showRefreshButton) { + Button('刷新显示(重新读单例)') + .fontSize(13) + .height(36) + .margin({ left: 12, right: 12, bottom: 8 }) + .onClick(() => { + this.refresh(true); + }) + } + + Scroll() { + Text(this.debugText.length > 0 + ? this.debugText + : '(暂无数据:请先完成 Flutter 登录或检查 MMKV)') + .fontSize(11) + .fontColor('#333333') + .fontFamily('monospace') + .lineHeight(16) + .width('100%') + } + .scrollBar(BarState.Auto) + .edgeEffect(EdgeEffect.Spring) + .layoutWeight(1) + .width('100%') + .padding({ left: 12, right: 12, top: 8, bottom: 12 }) + .backgroundColor('#F5F6F8') + .borderRadius(8) + .margin({ left: 12, right: 12, bottom: 12 }) + } + .layoutWeight(1) + .width('100%') + .alignItems(HorizontalAlign.Start) + } + + /** 重新读取两个单例的 dump 并刷新界面;可选同步打印控制台。 */ + private refresh(logToConsole: boolean): void { + const loginDump = LoginUserInfo.getInstance().debugDumpForTest(); + const settingsDump = AppSettings.getInstance().debugDump(); + const merged = `${loginDump}\n\n${settingsDump}`; + this.debugText = merged; + if (logToConsole) { + console.info(`[${TAG}]\n${merged}`); + } + } +} + +export default DebugInfoPanel; diff --git a/chat/demo/entry/src/main/module.json5 b/chat/demo/entry/src/main/module.json5 index 3ac3228..d206b10 100644 --- a/chat/demo/entry/src/main/module.json5 +++ b/chat/demo/entry/src/main/module.json5 @@ -19,9 +19,9 @@ "audioRecording", "audioPlayback" ], - "icon": "$media:layered_image", + "icon": "$media:app_icon", "label": "$string:EntryAbility_label", - "startWindowIcon": "$media:startIcon", + "startWindowIcon": "$media:app_icon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ @@ -70,14 +70,14 @@ // "when": "always" // } // }, -// { -// "name": "ohos.permission.CAMERA", -// "reason": "$string:permission_camera", -// "usedScene": { -// "abilities": ["EntryAbility"], -// "when": "always" -// } -// } + { + "name": "ohos.permission.CAMERA", + "reason": "$string:permission_camera", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "always" + } + } ] } } \ No newline at end of file diff --git a/chat/demo/entry/src/main/resources/base/element/color.json b/chat/demo/entry/src/main/resources/base/element/color.json index 3f8dd06..79cc747 100644 --- a/chat/demo/entry/src/main/resources/base/element/color.json +++ b/chat/demo/entry/src/main/resources/base/element/color.json @@ -7,6 +7,38 @@ { "name": "start_window_background2", "value": "#141516" + }, + { + "name": "hs_color_black", + "value": "#131212" + }, + { + "name": "hs_color_brand", + "value": "#FBF08B" + }, + { + "name": "hs_color_brand_link", + "value": "#ECD500" + }, + { + "name": "hs_color_text_secondary", + "value": "#5E5E66" + }, + { + "name": "hs_color_border_gray", + "value": "#DCDCDC" + }, + { + "name": "hs_color_bg_gray", + "value": "#F2F3F5" + }, + { + "name": "hs_color_purple_accent", + "value": "#7866F6" + }, + { + "name": "hs_color_white", + "value": "#FFFFFF" } ] -} \ No newline at end of file +} diff --git a/chat/demo/entry/src/main/resources/base/element/string.json b/chat/demo/entry/src/main/resources/base/element/string.json index 8e16c08..5284f7f 100644 --- a/chat/demo/entry/src/main/resources/base/element/string.json +++ b/chat/demo/entry/src/main/resources/base/element/string.json @@ -343,6 +343,70 @@ { "name": "relogin_button", "value": "Re-login" + }, + { + "name": "login_phone_button", + "value": "手机号登录/注册" + }, + { + "name": "login_agreement_prefix", + "value": "未注册手机号将自动注册。勾选即代表您阅读并同意" + }, + { + "name": "login_agreement_and", + "value": "和" + }, + { + "name": "login_user_agreement", + "value": "《用户协议》" + }, + { + "name": "login_privacy_policy", + "value": "《隐私政策》" + }, + { + "name": "login_operator_terms", + "value": "《运营商认证服务条款》" + }, + { + "name": "login_agreement_required", + "value": "请先阅读并勾选用户协议" + }, + { + "name": "login_third_party_coming_soon", + "value": "功能开发中,敬请期待" + }, + { + "name": "account_cancel_title", + "value": "注销账号" + }, + { + "name": "account_cancellation_warning", + "value": "注销后您将在海星出游客户端放弃下述权益:" + }, + { + "name": "account_cancellation_content", + "value": "• 您的身份、账户信息将清空\n\n• 会员权益将清空,专属优惠将失效\n\n• 行程记录和定制方案将清空且无法恢复\n\n• 与旅行顾问的聊天记录将被删除\n\n• 请确保所有订单已完结且无纠纷\n\n• 账户删除后历史订单可能产生的退款权益等将视作自动放弃\n\n• 销户后优惠券、积分等资产注销后将无法找回" + }, + { + "name": "account_cancellation_warm_tip", + "value": "• 建议您在注销前确认所有订单已完成、无售后问题\n\n• 如有未使用的权益或未出行行程,请提前处理\n\n• 注销后,如需再次使用,可重新注册账户" + }, + { + "name": "account_agreement_required", + "value": "请先阅读并同意协议" + }, + { + "name": "account_logoff_title", + "value": "确认注销账号吗?" + }, + { + "name": "account_logoff_message", + "value": "您正在申请注销账号,此操作无法撤销。请确认是否要继续?" + }, + { + "name": "account_cancel_success", + "value": "注销成功" } ] } diff --git a/chat/demo/entry/src/main/resources/base/media/app_icon.png b/chat/demo/entry/src/main/resources/base/media/app_icon.png index bcc7e41..aca35c2 100644 Binary files a/chat/demo/entry/src/main/resources/base/media/app_icon.png and b/chat/demo/entry/src/main/resources/base/media/app_icon.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/check_box_selected.png b/chat/demo/entry/src/main/resources/base/media/check_box_selected.png new file mode 100644 index 0000000..68b5351 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/check_box_selected.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/check_box_unselected.png b/chat/demo/entry/src/main/resources/base/media/check_box_unselected.png new file mode 100644 index 0000000..c3005b6 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/check_box_unselected.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/ic_launcher.png b/chat/demo/entry/src/main/resources/base/media/ic_launcher.png new file mode 100644 index 0000000..aca35c2 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/ic_launcher.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/ic_menu.png b/chat/demo/entry/src/main/resources/base/media/ic_menu.png new file mode 100644 index 0000000..a6a499a Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/ic_menu.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/icon_collect.png b/chat/demo/entry/src/main/resources/base/media/icon_collect.png new file mode 100644 index 0000000..d2356ad Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/icon_collect.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/icon_order.png b/chat/demo/entry/src/main/resources/base/media/icon_order.png new file mode 100644 index 0000000..b8c8de4 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/icon_order.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/icon_trip.png b/chat/demo/entry/src/main/resources/base/media/icon_trip.png new file mode 100644 index 0000000..5f8c3cf Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/icon_trip.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/left_icon_coupon.png b/chat/demo/entry/src/main/resources/base/media/left_icon_coupon.png new file mode 100644 index 0000000..ea81e08 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/left_icon_coupon.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/left_icon_user.png b/chat/demo/entry/src/main/resources/base/media/left_icon_user.png new file mode 100644 index 0000000..d9e6d04 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/left_icon_user.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/login_checkbox_checked.svg b/chat/demo/entry/src/main/resources/base/media/login_checkbox_checked.svg new file mode 100644 index 0000000..9b2ecdb --- /dev/null +++ b/chat/demo/entry/src/main/resources/base/media/login_checkbox_checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/chat/demo/entry/src/main/resources/base/media/login_checkbox_unchecked.svg b/chat/demo/entry/src/main/resources/base/media/login_checkbox_unchecked.svg new file mode 100644 index 0000000..b9336e3 --- /dev/null +++ b/chat/demo/entry/src/main/resources/base/media/login_checkbox_unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/chat/demo/entry/src/main/resources/base/media/login_icon_alipay.png b/chat/demo/entry/src/main/resources/base/media/login_icon_alipay.png new file mode 100644 index 0000000..1763ba9 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/login_icon_alipay.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/login_icon_wechat.png b/chat/demo/entry/src/main/resources/base/media/login_icon_wechat.png new file mode 100644 index 0000000..bf4bcb6 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/login_icon_wechat.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/remind.png b/chat/demo/entry/src/main/resources/base/media/remind.png new file mode 100644 index 0000000..ddfeb78 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/remind.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/settings_app_icon.png b/chat/demo/entry/src/main/resources/base/media/settings_app_icon.png new file mode 100644 index 0000000..aca35c2 Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/settings_app_icon.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/settings_warning.png b/chat/demo/entry/src/main/resources/base/media/settings_warning.png new file mode 100644 index 0000000..762b73a Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/settings_warning.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/startIcon.png b/chat/demo/entry/src/main/resources/base/media/startIcon.png index 205ad8b..aca35c2 100644 Binary files a/chat/demo/entry/src/main/resources/base/media/startIcon.png and b/chat/demo/entry/src/main/resources/base/media/startIcon.png differ diff --git a/chat/demo/entry/src/main/resources/base/media/union.png b/chat/demo/entry/src/main/resources/base/media/union.png new file mode 100644 index 0000000..762b73a Binary files /dev/null and b/chat/demo/entry/src/main/resources/base/media/union.png differ diff --git a/chat/demo/entry/src/main/resources/base/profile/main_pages.json b/chat/demo/entry/src/main/resources/base/profile/main_pages.json index 8a6e532..e8502f8 100644 --- a/chat/demo/entry/src/main/resources/base/profile/main_pages.json +++ b/chat/demo/entry/src/main/resources/base/profile/main_pages.json @@ -2,7 +2,16 @@ "src": [ "pages/SplashScreenPage", "pages/HomePage", + "pages/main/MainPage", + "pages/message/MessageCenterPage", + "pages/message/MessageDetailPage", + "pages/settings/AppSettingsPage", + "pages/settings/AccountCancellationPage", + "pages/settings/AboutPage", + "pages/settings/VersionInfoPage", + "pages/LoginPage", "pages/DevLoginPage", - "pages/SettingsPage" + "pages/SettingsPage", + "pages/FlutterBoostHostPage" ] } diff --git a/chat/demo/entry/src/main/resources/rawfile/welcome.mp4 b/chat/demo/entry/src/main/resources/rawfile/welcome.mp4 new file mode 100644 index 0000000..99bd8d8 Binary files /dev/null and b/chat/demo/entry/src/main/resources/rawfile/welcome.mp4 differ diff --git a/chat/demo/entry/src/main/resources/zh_CN/element/string.json b/chat/demo/entry/src/main/resources/zh_CN/element/string.json index 48e2c5c..c589a4c 100644 --- a/chat/demo/entry/src/main/resources/zh_CN/element/string.json +++ b/chat/demo/entry/src/main/resources/zh_CN/element/string.json @@ -10,11 +10,11 @@ }, { "name": "EntryAbility_label", - "value": "腾讯云通信" + "value": "海星出游" }, { "name": "demo_title", - "value": "腾讯云通信" + "value": "海星出游" }, { "name": "demo_conversation_tab_name", @@ -351,6 +351,70 @@ { "name": "relogin_button", "value": "重新登录" + }, + { + "name": "login_phone_button", + "value": "手机号登录/注册" + }, + { + "name": "login_agreement_prefix", + "value": "未注册手机号将自动注册。勾选即代表您阅读并同意" + }, + { + "name": "login_agreement_and", + "value": "和" + }, + { + "name": "login_user_agreement", + "value": "《用户协议》" + }, + { + "name": "login_privacy_policy", + "value": "《隐私政策》" + }, + { + "name": "login_operator_terms", + "value": "《运营商认证服务条款》" + }, + { + "name": "login_agreement_required", + "value": "请先阅读并勾选用户协议" + }, + { + "name": "login_third_party_coming_soon", + "value": "功能开发中,敬请期待" + }, + { + "name": "account_cancel_title", + "value": "注销账号" + }, + { + "name": "account_cancellation_warning", + "value": "注销后您将在海星出游客户端放弃下述权益:" + }, + { + "name": "account_cancellation_content", + "value": "• 您的身份、账户信息将清空\n\n• 会员权益将清空,专属优惠将失效\n\n• 行程记录和定制方案将清空且无法恢复\n\n• 与旅行顾问的聊天记录将被删除\n\n• 请确保所有订单已完结且无纠纷\n\n• 账户删除后历史订单可能产生的退款权益等将视作自动放弃\n\n• 销户后优惠券、积分等资产注销后将无法找回" + }, + { + "name": "account_cancellation_warm_tip", + "value": "• 建议您在注销前确认所有订单已完成、无售后问题\n\n• 如有未使用的权益或未出行行程,请提前处理\n\n• 注销后,如需再次使用,可重新注册账户" + }, + { + "name": "account_agreement_required", + "value": "请先阅读并同意协议" + }, + { + "name": "account_logoff_title", + "value": "确认注销账号吗?" + }, + { + "name": "account_logoff_message", + "value": "您正在申请注销账号,此操作无法撤销。请确认是否要继续?" + }, + { + "name": "account_cancel_success", + "value": "注销成功" } ] } diff --git a/chat/demo/flutter_module b/chat/demo/flutter_module new file mode 160000 index 0000000..27eea1a --- /dev/null +++ b/chat/demo/flutter_module @@ -0,0 +1 @@ +Subproject commit 27eea1a2182f540bd913a052797fa22d95957837 diff --git a/chat/demo/oh-package-lock.json5 b/chat/demo/oh-package-lock.json5 index 777254c..d245a74 100644 --- a/chat/demo/oh-package-lock.json5 +++ b/chat/demo/oh-package-lock.json5 @@ -6,15 +6,74 @@ "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { - "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0" + "@ohos/flutter_module@flutter_module/build/ohos/har/release/flutter_module.har": "@ohos/flutter_module@flutter_module/build/ohos/har/release/flutter_module.har", + "@ohos/flutter_ohos@flutter_module/build/ohos/har/release/flutter_embedding_release.har": "@ohos/flutter_ohos@flutter_module/build/ohos/har/release/flutter_embedding_release.har", + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "flutter_boost@flutter_module/build/ohos/har/release/flutter_boost.har": "flutter_boost@flutter_module/build/ohos/har/release/flutter_boost.har", + "flutter_native_arm64_v8a@flutter_module/build/ohos/har/release/arm64_v8a_release.har": "flutter_native_arm64_v8a@flutter_module/build/ohos/har/release/arm64_v8a_release.har", + "image_picker_ohos@flutter_module/build/ohos/har/release/image_picker_ohos.har": "image_picker_ohos@flutter_module/build/ohos/har/release/image_picker_ohos.har", + "webview_flutter_ohos@flutter_module/build/ohos/har/release/webview_flutter_ohos.har": "webview_flutter_ohos@flutter_module/build/ohos/har/release/webview_flutter_ohos.har" }, "packages": { + "@ohos/flutter_module@flutter_module/build/ohos/har/release/flutter_module.har": { + "name": "@ohos/flutter_module", + "version": "1.0.0", + "resolved": "flutter_module/build/ohos/har/release/flutter_module.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "", + "flutter_native_arm64_v8a": "", + "flutter_boost": "", + "image_picker_ohos": "", + "webview_flutter_ohos": "" + } + }, + "@ohos/flutter_ohos@flutter_module/build/ohos/har/release/flutter_embedding_release.har": { + "name": "@ohos/flutter_ohos", + "version": "1.0.0-e89c068aa5", + "resolved": "flutter_module/build/ohos/har/release/flutter_embedding_release.har", + "registryType": "local" + }, "@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" + }, + "flutter_boost@flutter_module/build/ohos/har/release/flutter_boost.har": { + "name": "flutter_boost", + "version": "4.6.5", + "resolved": "flutter_module/build/ohos/har/release/flutter_boost.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "file:har/flutter.har" + } + }, + "flutter_native_arm64_v8a@flutter_module/build/ohos/har/release/arm64_v8a_release.har": { + "name": "flutter_native_arm64_v8a", + "version": "1.0.0-e89c068aa5", + "resolved": "flutter_module/build/ohos/har/release/arm64_v8a_release.har", + "registryType": "local" + }, + "image_picker_ohos@flutter_module/build/ohos/har/release/image_picker_ohos.har": { + "name": "image_picker_ohos", + "version": "1.0.0", + "resolved": "flutter_module/build/ohos/har/release/image_picker_ohos.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "file:libs/flutter.har" + } + }, + "webview_flutter_ohos@flutter_module/build/ohos/har/release/webview_flutter_ohos.har": { + "name": "webview_flutter_ohos", + "version": "1.0.0", + "resolved": "flutter_module/build/ohos/har/release/webview_flutter_ohos.har", + "registryType": "local", + "dependencies": { + "@ohos/flutter_ohos": "", + "flutter_native_arm64_v8a": "" + } } } } \ No newline at end of file diff --git a/chat/demo/oh-package.json5 b/chat/demo/oh-package.json5 index 0afddcc..5494fd0 100644 --- a/chat/demo/oh-package.json5 +++ b/chat/demo/oh-package.json5 @@ -4,6 +4,20 @@ "dependencies": { }, "devDependencies": { - "@ohos/hamock": "1.0.0" + "@ohos/hamock": "1.0.0", + "@ohos/flutter_ohos": "", + "flutter_native_arm64_v8a": "", + "@ohos/flutter_module": "", + "flutter_boost": "", + "webview_flutter_ohos": "", + "image_picker_ohos": "" + }, + "overrides": { + "@ohos/flutter_ohos": "file:./flutter_module/build/ohos/har/release/flutter_embedding_release.har", + "flutter_native_arm64_v8a": "file:./flutter_module/build/ohos/har/release/arm64_v8a_release.har", + "@ohos/flutter_module": "file:./flutter_module/build/ohos/har/release/flutter_module.har", + "flutter_boost": "file:./flutter_module/build/ohos/har/release/flutter_boost.har", + "webview_flutter_ohos": "file:./flutter_module/build/ohos/har/release/webview_flutter_ohos.har", + "image_picker_ohos": "file:./flutter_module/build/ohos/har/release/image_picker_ohos.har" } } diff --git a/chat/uikit/oh-package-lock.json5 b/chat/uikit/oh-package-lock.json5 index fb96c7f..7550077 100644 --- a/chat/uikit/oh-package-lock.json5 +++ b/chat/uikit/oh-package-lock.json5 @@ -8,15 +8,14 @@ "specifiers": { "@tencent/mmkv@1.3.5": "@tencent/mmkv@1.3.5", "@tencentcloud/atomicx@../../atomic_x": "@tencentcloud/atomicx@../../atomic_x", - "@tencentcloud/atomicxcore@4.1.0": "@tencentcloud/atomicxcore@4.1.0", - "@tencentcloud/atomicxcore@^3.6.0": "@tencentcloud/atomicxcore@4.1.0", + "@tencentcloud/atomicxcore@4.1.0": "@tencentcloud/atomicxcore@4.2.0", + "@tencentcloud/atomicxcore@^3.6.0": "@tencentcloud/atomicxcore@4.2.0", "@tencentcloud/imsdk@^8.6.7020": "@tencentcloud/imsdk@8.9.7542", "@tencentcloud/liteavsdk_professional@^13.2.1135": "@tencentcloud/liteavsdk_professional@13.2.8732", - "@tencentcloud/rtcroomengine@^4.1.0": "@tencentcloud/rtcroomengine@4.1.0", "libTXCDownloadProxy.so@../demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": "libTXCDownloadProxy.so@../demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy", "libTXCThumbPlayer.so@../demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export": "libTXCThumbPlayer.so@../demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", + "libatomicengine.so@../demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": "libatomicengine.so@../demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", "libliteavsdk.so@../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": "libliteavsdk.so@../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", - "librtcroomengine.so@../demo/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine": "librtcroomengine.so@../demo/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine", "txcdownloadproxy@../demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": "txcdownloadproxy@../demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har", "txcthumbplayer@../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har": "txcthumbplayer@../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/libs/txcthumbplayer.har" }, @@ -43,15 +42,15 @@ }, "packageType": "InterfaceHar" }, - "@tencentcloud/atomicxcore@4.1.0": { + "@tencentcloud/atomicxcore@4.2.0": { "name": "@tencentcloud/atomicxcore", - "version": "4.1.0", - "integrity": "sha512-Hob9rrY3qkeraMguWJovDAKqtEI7gBx7e7GhcuXbtuOa7vxSaCvvUCpNf9KrymXXVM82WLtMOhgh2NCQdKJP/Q==", - "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.1.0.har", + "version": "4.2.0", + "integrity": "sha512-+4puTeVVcPmzuZ/nKtpZnWM9uOCPTLmtcvKQ2YgdP0TsXOSWAhC03TqyNM+UwVDZMSMEJF/2RbpSBN5HyBklaw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/atomicxcore/-/atomicxcore-4.2.0.har", "registryType": "ohpm", "dependencies": { + "libatomicengine.so": "file:./src/main/cpp/types/libatomicengine", "@tencentcloud/imsdk": "^8.6.7020", - "@tencentcloud/rtcroomengine": "^4.1.0", "@tencentcloud/liteavsdk_professional": "^13.2.1135" } }, @@ -73,17 +72,6 @@ "txcthumbplayer": "file:./libs/txcthumbplayer.har" } }, - "@tencentcloud/rtcroomengine@4.1.0": { - "name": "@tencentcloud/rtcroomengine", - "version": "4.1.0", - "integrity": "sha512-lspTdW4LmIdo4nvhUsr8362qwnzYSGkT0aENSx7014rKwxJ62ksTBIxkR/SEEe9PvTjVhg8aqKMb2YNx2wD9KA==", - "resolved": "https://ohpm.openharmony.cn/ohpm/@tencentcloud/rtcroomengine/-/rtcroomengine-4.1.0.har", - "registryType": "ohpm", - "dependencies": { - "librtcroomengine.so": "file:./src/main/cpp/types/librtcroomengine", - "@tencentcloud/liteavsdk_professional": "^13.2.1135" - } - }, "libTXCDownloadProxy.so@../demo/oh_modules/.ohpm/txcdownloadproxy@z8ya9mi+d+crsytgzo2ggrpi6qqkquxaszy+cuievaa=/oh_modules/txcdownloadproxy/src/main/cpp/types/libtxcdownloadproxy": { "name": "libTXCDownloadProxy.so", "version": "1.0.0", @@ -96,16 +84,16 @@ "resolved": "../demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/src/main/ets/napi_export", "registryType": "local" }, - "libliteavsdk.so@../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { - "name": "libliteavsdk.so", + "libatomicengine.so@../demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine": { + "name": "libatomicengine.so", "version": "1.0.0", - "resolved": "../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", + "resolved": "../demo/oh_modules/.ohpm/@tencentcloud+atomicxcore@4.2.0/oh_modules/@tencentcloud/atomicxcore/src/main/cpp/types/libatomicengine", "registryType": "local" }, - "librtcroomengine.so@../demo/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine": { - "name": "librtcroomengine.so", + "libliteavsdk.so@../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk": { + "name": "libliteavsdk.so", "version": "1.0.0", - "resolved": "../demo/oh_modules/.ohpm/@tencentcloud+rtcroomengine@4.1.0/oh_modules/@tencentcloud/rtcroomengine/src/main/cpp/types/librtcroomengine", + "resolved": "../demo/oh_modules/.ohpm/@tencentcloud+liteavsdk_professional@13.2.8732/oh_modules/@tencentcloud/liteavsdk_professional/src/main/cpp/types/libliteavsdk", "registryType": "local" }, "txcdownloadproxy@../demo/oh_modules/.ohpm/txcthumbplayer@8qxg+osmimruot1qz10oxzpz64dyuqgghddwygckeho=/oh_modules/txcthumbplayer/libs/TXCDownloadProxy.har": {