├── art ├── demo0.jpg ├── demo1.jpg ├── demo2.jpg ├── demo3.jpg ├── demo4.jpg ├── demo5.jpg └── demo6.jpg ├── entry ├── .gitignore ├── src │ ├── main │ │ ├── ets │ │ │ ├── pages │ │ │ │ ├── chat │ │ │ │ │ ├── ChatPageParam.ets │ │ │ │ │ ├── ChatContentItemData.ets │ │ │ │ │ ├── ChatFuntionBar.ets │ │ │ │ │ └── ChatPage.ets │ │ │ │ ├── discovery │ │ │ │ │ └── Discovery.ets │ │ │ │ ├── home │ │ │ │ │ └── Home.ets │ │ │ │ ├── contact │ │ │ │ │ ├── DataItem.ets │ │ │ │ │ └── Contact.ets │ │ │ │ ├── qrcode │ │ │ │ │ └── MyQrCodePage.ets │ │ │ │ ├── Index.ets │ │ │ │ ├── mine │ │ │ │ │ └── Mine.ets │ │ │ │ ├── search │ │ │ │ │ └── SearchPage.ets │ │ │ │ └── HardwareIndexPage.ets │ │ │ ├── images │ │ │ │ ├── main_page_index0_nor.png │ │ │ │ ├── main_page_index0_pre.png │ │ │ │ ├── main_page_index1_nor.png │ │ │ │ ├── main_page_index1_pre.png │ │ │ │ ├── main_page_index2_nor.png │ │ │ │ ├── main_page_index2_pre.png │ │ │ │ ├── main_page_index3_nor.png │ │ │ │ └── main_page_index3_pre.png │ │ │ ├── component │ │ │ │ ├── ListContactItem.ets │ │ │ │ ├── ListMenuItem.ets │ │ │ │ ├── WechatToolbar.ets │ │ │ │ ├── ListChatItem.ets │ │ │ │ ├── Toolbar.ets │ │ │ │ ├── ListChatContentLeftItem.ets │ │ │ │ ├── ListChatContentRightItem.ets │ │ │ │ └── SoundRecordComponent.ets │ │ │ ├── utils │ │ │ │ ├── Toast.ets │ │ │ │ ├── Log.ts │ │ │ │ ├── permissionMananger.ets │ │ │ │ ├── PermissionUtils.ets │ │ │ │ ├── AudioRendererManager.ets │ │ │ │ ├── WindowManager.ets │ │ │ │ ├── SpeechRecognizerManager.ets │ │ │ │ ├── AudioCapturerManager.ets │ │ │ │ └── PhotoPickerUtils.ets │ │ │ └── entryability │ │ │ │ └── EntryAbility.ets │ │ ├── resources │ │ │ ├── base │ │ │ │ ├── media │ │ │ │ │ ├── ic_add.png │ │ │ │ │ ├── ic_tag.png │ │ │ │ │ ├── icon.png │ │ │ │ │ ├── ic_game.png │ │ │ │ │ ├── ic_more.png │ │ │ │ │ ├── ic_scan.png │ │ │ │ │ ├── ic_shoot.png │ │ │ │ │ ├── ic_voice.png │ │ │ │ │ ├── ic_camera.png │ │ │ │ │ ├── ic_card_pkg.png │ │ │ │ │ ├── ic_collect.png │ │ │ │ │ ├── ic_delete.png │ │ │ │ │ ├── ic_hahaha.png │ │ │ │ │ ├── ic_its_over.png │ │ │ │ │ ├── ic_listen.png │ │ │ │ │ ├── ic_location.png │ │ │ │ │ ├── ic_moment.png │ │ │ │ │ ├── ic_qrcode.png │ │ │ │ │ ├── ic_red_pkg.png │ │ │ │ │ ├── ic_search.png │ │ │ │ │ ├── ic_service.png │ │ │ │ │ ├── ic_setting.png │ │ │ │ │ ├── wechat_logo.png │ │ │ │ │ ├── ic_back_black.png │ │ │ │ │ ├── ic_emoji_pack.png │ │ │ │ │ ├── ic_face_icon.png │ │ │ │ │ ├── ic_group_chat.png │ │ │ │ │ ├── ic_moment_pic.png │ │ │ │ │ ├── ic_new_friend.png │ │ │ │ │ ├── ic_use_voice.png │ │ │ │ │ ├── ic_user_head1.jpg │ │ │ │ │ ├── ic_user_head2.jpg │ │ │ │ │ ├── ic_user_head3.jpg │ │ │ │ │ ├── ic_user_head4.jpg │ │ │ │ │ ├── ic_user_head5.jpg │ │ │ │ │ ├── ic_user_head6.jpg │ │ │ │ │ ├── ic_user_head7.jpg │ │ │ │ │ ├── ic_voice_call.png │ │ │ │ │ ├── ic_miniprogram.png │ │ │ │ │ ├── ic_my_collection.png │ │ │ │ │ ├── ic_toolbar_more.png │ │ │ │ │ ├── ic_translation.png │ │ │ │ │ ├── ic_use_keyboard.png │ │ │ │ │ ├── ic_voice_input.png │ │ │ │ │ ├── ic_voice_msg_left.png │ │ │ │ │ ├── ic_voice_msg_right.png │ │ │ │ │ ├── ic_official_account.png │ │ │ │ │ └── ic_wechat_qrcode_logo.png │ │ │ │ ├── element │ │ │ │ │ ├── color.json │ │ │ │ │ └── string.json │ │ │ │ └── profile │ │ │ │ │ └── main_pages.json │ │ │ ├── zh_CN │ │ │ │ └── element │ │ │ │ │ └── string.json │ │ │ └── en_US │ │ │ │ └── element │ │ │ │ └── string.json │ │ └── module.json5 │ └── ohosTest │ │ ├── resources │ │ └── base │ │ │ ├── profile │ │ │ └── test_pages.json │ │ │ ├── media │ │ │ └── icon.png │ │ │ └── element │ │ │ ├── color.json │ │ │ └── string.json │ │ ├── ets │ │ ├── test │ │ │ ├── List.test.ets │ │ │ └── Ability.test.ets │ │ ├── testability │ │ │ ├── pages │ │ │ │ └── Index.ets │ │ │ └── TestAbility.ets │ │ └── testrunner │ │ │ └── OpenHarmonyTestRunner.ts │ │ └── module.json5 ├── hvigorfile.ts ├── build-profile.json5 └── oh-package.json5 ├── hvigor └── hvigor-config.json5 ├── AppScope ├── resources │ └── base │ │ ├── media │ │ └── app_icon.png │ │ └── element │ │ └── string.json └── app.json5 ├── hvigorfile.ts ├── .gitignore ├── oh-package.json5 ├── README.md └── oh-package-lock.json5 /art/demo0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo0.jpg -------------------------------------------------------------------------------- /art/demo1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo1.jpg -------------------------------------------------------------------------------- /art/demo2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo2.jpg -------------------------------------------------------------------------------- /art/demo3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo3.jpg -------------------------------------------------------------------------------- /art/demo4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo4.jpg -------------------------------------------------------------------------------- /art/demo5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo5.jpg -------------------------------------------------------------------------------- /art/demo6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/art/demo6.jpg -------------------------------------------------------------------------------- /entry/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /build 5 | /.cxx 6 | /.test -------------------------------------------------------------------------------- /hvigor/hvigor-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.0", 3 | "dependencies": { 4 | } 5 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/chat/ChatPageParam.ets: -------------------------------------------------------------------------------- 1 | export default interface ChatPageParam { 2 | name: string 3 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/profile/test_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "testability/pages/Index" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /AppScope/resources/base/media/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/AppScope/resources/base/media/app_icon.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_add.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_tag.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/icon.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_game.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_more.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_scan.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_shoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_shoot.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_voice.png -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/test/List.test.ets: -------------------------------------------------------------------------------- 1 | import abilityTest from './Ability.test' 2 | 3 | export default function testsuite() { 4 | abilityTest() 5 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/ohosTest/resources/base/media/icon.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index0_nor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index0_nor.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index0_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index0_pre.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index1_nor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index1_nor.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index1_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index1_pre.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index2_nor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index2_nor.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index2_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index2_pre.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index3_nor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index3_nor.png -------------------------------------------------------------------------------- /entry/src/main/ets/images/main_page_index3_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/ets/images/main_page_index3_pre.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_camera.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_card_pkg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_card_pkg.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_collect.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_delete.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_hahaha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_hahaha.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_its_over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_its_over.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_listen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_listen.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_location.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_moment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_moment.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_qrcode.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_red_pkg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_red_pkg.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_search.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_service.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_setting.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/wechat_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/wechat_logo.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_back_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_back_black.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_emoji_pack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_emoji_pack.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_face_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_face_icon.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_group_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_group_chat.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_moment_pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_moment_pic.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_new_friend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_new_friend.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_use_voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_use_voice.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head1.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head2.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head3.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head4.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head5.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head6.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_user_head7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_user_head7.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_voice_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_voice_call.png -------------------------------------------------------------------------------- /AppScope/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "app_name", 5 | "value": "Wechat_HarmonyOS" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_miniprogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_miniprogram.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_my_collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_my_collection.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_toolbar_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_toolbar_more.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_translation.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_use_keyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_use_keyboard.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_voice_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_voice_input.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FFFFFF" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_voice_msg_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_voice_msg_left.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_voice_msg_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_voice_msg_right.png -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FFFFFF" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /hvigorfile.ts: -------------------------------------------------------------------------------- 1 | // Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. 2 | export { appTasks } from '@ohos/hvigor-ohos-plugin'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /local.properties 4 | /.idea 5 | **/build 6 | /.hvigor 7 | .cxx 8 | /.clangd 9 | /.clang-format 10 | /.clang-tidy 11 | **/.test -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_official_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_official_account.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/ic_wechat_qrcode_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ausboyue/Wechat_HarmonyOS/HEAD/entry/src/main/resources/base/media/ic_wechat_qrcode_logo.png -------------------------------------------------------------------------------- /entry/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | // Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. 2 | export { hapTasks } from '@ohos/hvigor-ohos-plugin'; 3 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/chat/ChatContentItemData.ets: -------------------------------------------------------------------------------- 1 | export interface ChatContentItemData { 2 | name?: string 3 | voicePath?: string 4 | headSrc?: Resource 5 | msg?: string 6 | img?: Resource 7 | msgType?: number 8 | self?: Boolean 9 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/profile/main_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "pages/Index", 4 | "pages/search/SearchPage", 5 | "pages/chat/ChatPage", 6 | "pages/qrcode/MyQrCodePage", 7 | "pages/HardwareIndexPage" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /AppScope/app.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "bundleName": "cn.icheny.wechat", 4 | "vendor": "example", 5 | "versionCode": 1000000, 6 | "versionName": "1.0.0", 7 | "icon": "$media:app_icon", 8 | "label": "$string:app_name" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /entry/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "apiType": 'stageMode', 3 | "buildOption": { 4 | }, 5 | "targets": [ 6 | { 7 | "name": "default", 8 | "runtimeOS": "HarmonyOS" 9 | }, 10 | { 11 | "name": "ohosTest", 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /entry/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "license": "", 3 | "devDependencies": {}, 4 | "author": "", 5 | "name": "entry", 6 | "description": "Please describe the basic information.", 7 | "main": "", 8 | "version": "1.0.0", 9 | "dependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /entry/src/main/resources/zh_CN/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "模块描述" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "微信鸿蒙版" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/main/resources/en_US/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "module description" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "WechatHarmony" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_test_desc", 5 | "value": "test ability description" 6 | }, 7 | { 8 | "name": "TestAbility_desc", 9 | "value": "the test ability" 10 | }, 11 | { 12 | "name": "TestAbility_label", 13 | "value": "test label" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.0", 3 | "license": "", 4 | "devDependencies": { 5 | "@ohos/hypium": "1.0.6" 6 | }, 7 | "author": "", 8 | "name": "wechat_harmonyos", 9 | "description": "A WeChat DEMO application developed based on Harmony OS.", 10 | "main": "", 11 | "version": "1.0.0", 12 | "dependencies": { 13 | "dayjs": "^1.11.7" 14 | }, 15 | "dynamicDependencies": {} 16 | } -------------------------------------------------------------------------------- /entry/src/main/ets/component/ListContactItem.ets: -------------------------------------------------------------------------------- 1 | @Preview 2 | @Component 3 | export default struct ListContactItem { 4 | private head: string | PixelMap | Resource = "" 5 | private name: string = "" 6 | 7 | build() { 8 | Row() { 9 | Image(this.head) 10 | .width(46) 11 | .height(46) 12 | .borderRadius(4) 13 | .margin({ left: 20, right: 15 }) 14 | .objectFit(ImageFit.Cover) 15 | 16 | Text(this.name) 17 | .fontSize(19) 18 | .fontWeight(500) 19 | .width("100%") 20 | .layoutWeight(1) 21 | .maxLines(1) 22 | .textOverflow({ overflow: TextOverflow.Ellipsis }) 23 | .margin({ right: 30 }) 24 | .fontColor(Color.Black) 25 | } 26 | .backgroundColor(Color.White) 27 | .width("100%") 28 | .height(60) 29 | } 30 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/Toast.ets: -------------------------------------------------------------------------------- 1 | import promptAction from '@ohos.promptAction'; 2 | import Log from './Log'; 3 | 4 | type Msg = string | Resource | number | boolean 5 | 6 | export class Toast { 7 | static show(msg: Msg) { 8 | Toast.showTime(msg, 2000) 9 | } 10 | 11 | static showLong(msg: Msg) { 12 | Toast.showTime(msg, 4000) 13 | } 14 | 15 | static showTime(msg: Msg, duration: number = 4000) { 16 | try { 17 | promptAction.showToast({ 18 | message: (typeof msg === "number" || typeof msg === "boolean") ? 19 | String(msg) as string : msg, 20 | duration: duration, 21 | bottom: "center", 22 | }) 23 | } catch (error) { 24 | Log.e(`Toast --> Catch error! The error code is ${error.code}, The error message is ${error.message}`) 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "module description" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "微信鸿蒙版" 14 | }, 15 | { 16 | "name": "net_permission_reason", 17 | "value": "APP需要联网" 18 | }, 19 | { 20 | "name": "camera_permission_reason", 21 | "value": "需要相机权限进行照片上传" 22 | }, 23 | { 24 | "name": "read_media_permission_reason", 25 | "value": "需要访问相册进行图片上传" 26 | }, 27 | { 28 | "name": "write_media_permission_reason", 29 | "value": "需要拍照进行照片上传" 30 | }, 31 | { 32 | "name": "net_info_permission_reason", 33 | "value": "需要解析网络状态" 34 | }, 35 | { 36 | "name": "voice_reason", 37 | "value": "用于获取用户的录音" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 微信鸿蒙版 2 | 3 | 基于最新鸿蒙API 15(HarmonyOS NEXT)和ArkTS开发的高仿微信APP 4 | 5 | ![demo0.jpg](art%2Fdemo0.jpg) 6 | 7 | ### 特点 8 | 9 | 1. 高仿程度98+%(自评) 10 | 2. 支持沉浸式状态栏 11 | 3. 支持Path绘制 12 | 4. 抹除编辑框点击效果的坑 13 | 14 | 15 | ### 已开发页面: 16 | 17 | #### 微信Tab 18 | ![demo1.jpg](art%2Fdemo1.jpg) 19 | 20 | #### 通讯录Tab 21 | ![demo2.jpg](art%2Fdemo2.jpg) 22 | 23 | #### 发现Tab 24 | ![demo3.jpg](art%2Fdemo3.jpg) 25 | 26 | #### 我Tab 27 | ![demo4.jpg](art%2Fdemo4.jpg) 28 | 29 | #### 聊天页 30 | ![demo5.jpg](art%2Fdemo5.jpg) 31 | 32 | #### 搜索页 33 | ![demo6.jpg](art%2Fdemo6.jpg) 34 | 35 | ### 评语 36 | 37 | 1. 鸿蒙开发设计结合了Android,IOS,FLutter,RN等平台和方案的优点设计 38 | 2. TS(ArkTS)语言壁垒在鸿蒙这块降得很低,学习成本低,很好上手 39 | 3. 总得来说,非常nice,效率和舒适度比Android和Flutter高很多,内置控件和容器非常丰富,并且可定制化能力也很高 40 | 4. 未来可期,同时期待Flutter早日完美适配鸿蒙 41 | 42 | ### 后续目标 43 | 44 | 1. 微信朋友圈 45 | 2. 聊天菜单(相册,拍摄...)组件栏 46 | 3. 语音|视频页面(现已支持发送和播放语音消息) 47 | 4. 支持群聊头像 48 | 5. 支持图片,红包等聊天内容类型(现已支持图片类型) 49 | 6. 二维码扫描 50 | 51 | **由于近期有别的重要计划安排,本项目后续进度会放缓,有兴趣的朋友可以参与进来一起开发哈** 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/testability/pages/Index.ets: -------------------------------------------------------------------------------- 1 | import hilog from '@ohos.hilog'; 2 | 3 | @Entry 4 | @Component 5 | struct Index { 6 | aboutToAppear() { 7 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility index aboutToAppear'); 8 | } 9 | @State message: string = 'Hello World' 10 | build() { 11 | Row() { 12 | Column() { 13 | Text(this.message) 14 | .fontSize(50) 15 | .fontWeight(FontWeight.Bold) 16 | Button() { 17 | Text('next page') 18 | .fontSize(20) 19 | .fontWeight(FontWeight.Bold) 20 | }.type(ButtonType.Capsule) 21 | .margin({ 22 | top: 20 23 | }) 24 | .backgroundColor('#0D9FFB') 25 | .width('35%') 26 | .height('5%') 27 | .onClick(()=>{ 28 | }) 29 | } 30 | .width('100%') 31 | } 32 | .height('100%') 33 | } 34 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "entry_test", 4 | "type": "feature", 5 | "description": "$string:module_test_desc", 6 | "mainElement": "TestAbility", 7 | "deviceTypes": [ 8 | "phone", 9 | "tablet" 10 | ], 11 | "deliveryWithInstall": true, 12 | "installationFree": false, 13 | "pages": "$profile:test_pages", 14 | "abilities": [ 15 | { 16 | "name": "TestAbility", 17 | "srcEntry": "./ets/testability/TestAbility.ets", 18 | "description": "$string:TestAbility_desc", 19 | "icon": "$media:icon", 20 | "label": "$string:TestAbility_label", 21 | "exported": true, 22 | "startWindowIcon": "$media:icon", 23 | "startWindowBackground": "$color:start_window_background", 24 | "skills": [ 25 | { 26 | "actions": [ 27 | "action.system.home" 28 | ], 29 | "entities": [ 30 | "entity.system.home" 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /entry/src/main/ets/component/ListMenuItem.ets: -------------------------------------------------------------------------------- 1 | @Preview 2 | @Component 3 | export default struct ListMenuItem { 4 | private iconSrc: Resource = $r('app.media.icon') 5 | private title: string = "朋友圈" 6 | private news: string = "" 7 | private newsIconSrc?: Resource 8 | 9 | build() { 10 | Row() { 11 | Image(this.iconSrc) 12 | .width(25) 13 | .height(25) 14 | .margin({ right: 18 }) 15 | Text(this.title) 16 | .fontColor(Color.Black) 17 | .fontSize(19) 18 | .margin({ right: 10 }) 19 | Blank() 20 | Text(this.news) 21 | .fontColor(Color.Gray) 22 | .fontSize(16) 23 | .margin({ right: 6 }) 24 | Image(this.newsIconSrc) 25 | .width(this.newsIconSrc === null ? 0 : 35) 26 | .height(this.newsIconSrc === null ? 0 : 35) 27 | .margin({ right: 6 }) 28 | .borderRadius(4) 29 | Image($r("app.media.ic_more")) 30 | .width(9) 31 | .height(16.364) 32 | } 33 | .width("100%") 34 | .height(60) 35 | .backgroundColor("#fff") 36 | .padding({ left: 15, right: 15 }) 37 | } 38 | } -------------------------------------------------------------------------------- /oh-package-lock.json5: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "stableOrder": true 4 | }, 5 | "lockfileVersion": 3, 6 | "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", 7 | "specifiers": { 8 | "@ohos/hypium@1.0.6": "@ohos/hypium@1.0.6", 9 | "dayjs@^1.11.7": "dayjs@1.11.7" 10 | }, 11 | "packages": { 12 | "@ohos/hypium@1.0.6": { 13 | "name": "@ohos/hypium", 14 | "version": "1.0.6", 15 | "integrity": "sha512-bb3DWeWhYrFqj9mPFV3yZQpkm36kbcK+YYaeY9g292QKSjOdmhEIQR2ULPvyMsgSR4usOBf5nnYrDmaCCXirgQ==", 16 | "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hypium/-/hypium-1.0.6.tgz", 17 | "shasum": "3f5fed65372633233264b3447705b0831dfe7ea1", 18 | "registryType": "ohpm" 19 | }, 20 | "dayjs@1.11.7": { 21 | "name": "dayjs", 22 | "version": "1.11.7", 23 | "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", 24 | "resolved": "https://repo.harmonyos.com/ohpm/dayjs/-/dayjs-1.11.7.tgz", 25 | "shasum": "4b296922642f70999544d1144a2c25730fce63e2", 26 | "registryType": "ohpm" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /entry/src/main/ets/component/WechatToolbar.ets: -------------------------------------------------------------------------------- 1 | import router from '@ohos.router' 2 | import prompt from '@ohos.promptAction' 3 | import WindowManager from '../utils/WindowManager' 4 | 5 | @Preview 6 | @Component 7 | export default struct WechatToolbar { 8 | private title: string = "发现" 9 | 10 | build() { 11 | Column() { 12 | Stack() { 13 | Text(this.title) 14 | .fontColor(Color.Black) 15 | .fontSize(19) 16 | .fontWeight(500) 17 | 18 | Row() { 19 | Image($r("app.media.ic_search")) 20 | .width(22) 21 | .height(22) 22 | .margin({ right: 20 }) 23 | .onClick(() => { 24 | router.pushUrl({ url: 'pages/search/SearchPage' }) 25 | }) 26 | Image($r('app.media.ic_add')) 27 | .width(25) 28 | .height(25) 29 | .margin({ right: 18 }) 30 | .onClick(() => { 31 | prompt.showToast({ message: "功能正在开发中...", duration: 2000 }) 32 | router.pushUrl({ url: 'pages/HardwareIndexPage' }) 33 | }) 34 | } 35 | .height("100%") 36 | .width("100%") 37 | .alignItems(VerticalAlign.Center) 38 | .justifyContent(FlexAlign.End) 39 | 40 | } 41 | .width("100%") 42 | .height(53) 43 | }.backgroundColor("#f0f0f0") 44 | .padding({ top: WindowManager.statusBarHeight }) 45 | } 46 | } -------------------------------------------------------------------------------- /entry/src/main/ets/component/ListChatItem.ets: -------------------------------------------------------------------------------- 1 | @Preview 2 | @Component 3 | export default struct ListChatItem { 4 | data: ChatItemData = { headSrc: $r("app.media.ic_user_head1"), 5 | nickname: "同事小李", 6 | content: "领导,报给写好了,请过目", 7 | time: "14:12" 8 | } 9 | 10 | build() { 11 | Row() { 12 | Image(this.data.headSrc) 13 | .width(60) 14 | .height(60) 15 | .margin({ right: 15 }) 16 | .objectFit(ImageFit.Cover) 17 | .borderRadius(6) 18 | .borderWidth(2) 19 | .borderColor("#f0f0f0") 20 | Column() { 21 | Text(this.data.nickname) 22 | .fontColor(Color.Black) 23 | .fontSize(19) 24 | .fontWeight(500) 25 | .maxLines(1) 26 | .textOverflow({ overflow: TextOverflow.Ellipsis }) 27 | .width("100%") 28 | 29 | Text(this.data.content) 30 | .fontColor(Color.Gray) 31 | .fontSize(15) 32 | .width("100%") 33 | .maxLines(1) 34 | .textOverflow({ overflow: TextOverflow.Ellipsis }) 35 | .margin({ top: 7 }) 36 | 37 | }.layoutWeight(1) 38 | .margin({ right: 30 }) 39 | .alignItems(HorizontalAlign.Start) 40 | 41 | Text(this.data.time) 42 | .fontColor(Color.Gray) 43 | .fontSize(14) 44 | .alignSelf(ItemAlign.Start) 45 | .margin({ top: 15, right: 6 }) 46 | } 47 | .width("100%") 48 | .height(80) 49 | .backgroundColor(Color.White) 50 | .padding({ left: 15, right: 15 }) 51 | } 52 | } 53 | 54 | export interface ChatItemData { 55 | headSrc?: Resource 56 | nickname?: string 57 | content?: string 58 | time?: string 59 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/discovery/Discovery.ets: -------------------------------------------------------------------------------- 1 | import ListMenuItem from '../../component/ListMenuItem' 2 | import WechatToolbar from '../../component/WechatToolbar' 3 | 4 | @Preview 5 | @Component 6 | export default struct Discovery { 7 | build() { 8 | Column() { 9 | WechatToolbar({ title: "发现" }) 10 | 11 | ListMenuItem({ title: "朋友圈", iconSrc: $r('app.media.ic_moment') }) 12 | 13 | Divider() 14 | .vertical(false) 15 | .color("#f3f3f3") 16 | .strokeWidth(10) 17 | .lineCap(LineCapStyle.Round) 18 | 19 | ListMenuItem({ title: "扫一扫", 20 | iconSrc: $r('app.media.ic_scan') }) 21 | .onClick(() => { 22 | }) 23 | 24 | Divider() 25 | .vertical(false) 26 | .color("#f3f3f3") 27 | .strokeWidth(1) 28 | .lineCap(LineCapStyle.Round) 29 | 30 | ListMenuItem({ title: "听一听", 31 | iconSrc: $r('app.media.ic_listen') 32 | }) 33 | 34 | Divider() 35 | .vertical(false) 36 | .color("#f3f3f3") 37 | .strokeWidth(10) 38 | .lineCap(LineCapStyle.Round) 39 | 40 | ListMenuItem({ 41 | title: "游戏", 42 | news: "朋友瞬杀时刻", 43 | newsIconSrc: $r('app.media.ic_user_head1'), 44 | iconSrc: $r('app.media.ic_game') 45 | }) 46 | 47 | Divider() 48 | .vertical(false) 49 | .color("#f3f3f3") 50 | .strokeWidth(10) 51 | .lineCap(LineCapStyle.Round) 52 | 53 | ListMenuItem({ title: "小程序", 54 | iconSrc: $r('app.media.ic_miniprogram') 55 | }) 56 | 57 | } 58 | .width("100%") 59 | .height("100%") 60 | .backgroundColor("#f0f0f0") 61 | } 62 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/Log.ts: -------------------------------------------------------------------------------- 1 | import { hilog } from '@kit.PerformanceAnalysisKit' 2 | 3 | class Log { 4 | private _domain: number 5 | private _loggable: boolean = true 6 | private _tag: string = 'Logger' 7 | 8 | constructor() { 9 | this._domain = 0xFF00 10 | } 11 | 12 | set tag(tag) { 13 | if (tag) { 14 | // It is recommended to set it when the app starts 15 | this._tag = tag 16 | } 17 | } 18 | 19 | set loggable(loggable) { 20 | this._loggable = loggable 21 | } 22 | 23 | d(format: string, ...args: any[]): void { 24 | this.dt(this._tag, format, args) 25 | } 26 | 27 | dt(tag: string = this._tag, format: string, ...args: any[]): void { 28 | if (this._loggable) { 29 | hilog.debug(this._domain, tag, format, args) 30 | } 31 | } 32 | 33 | i(format: string, ...args: any[]): void { 34 | this.it(this._tag, format, args) 35 | } 36 | 37 | it(tag: string = this._tag, format: string, ...args: any[]): void { 38 | if (this._loggable) { 39 | hilog.info(this._domain, tag, format, args) 40 | } 41 | } 42 | 43 | w(format: string, ...args: any[]): void { 44 | this.wt(this._tag, format, args) 45 | } 46 | 47 | wt(tag: string = this._tag, format: string, ...args: any[]): void { 48 | if (this._loggable) { 49 | hilog.warn(this._domain, tag, format, args) 50 | } 51 | } 52 | 53 | e(format: string, ...args: any[]): void { 54 | this.et(this._tag, format, args) 55 | } 56 | 57 | et(tag: string = this._tag, format: string, ...args: any[]): void { 58 | if (this._loggable) { 59 | hilog.error(this._domain, tag, format, args) 60 | } 61 | } 62 | } 63 | 64 | export default new Log() -------------------------------------------------------------------------------- /entry/src/main/ets/component/Toolbar.ets: -------------------------------------------------------------------------------- 1 | import router from '@ohos.router' 2 | import WindowManager from '../utils/WindowManager' 3 | import { Toast } from '../utils/Toast' 4 | 5 | @Preview 6 | @Component 7 | export default struct Toolbar { 8 | private title: string = "发现" 9 | private backSrc: string | PixelMap | Resource = $r("app.media.ic_back_black") 10 | private subTitle: string = "更多" 11 | private subIconSrc: string | PixelMap | Resource = $r("app.media.ic_toolbar_more") 12 | 13 | build() { 14 | Column() { 15 | Row() { 16 | Image(this.backSrc) 17 | .width(22) 18 | .height(22) 19 | .margin({ left: 15 }) 20 | .onClick(() => { 21 | router.back() 22 | }) 23 | 24 | Text(this.title) 25 | .fontColor(Color.Black) 26 | .fontSize(19) 27 | .fontWeight(500) 28 | 29 | if (this.subIconSrc === null) { 30 | Text(this.subTitle) 31 | .fontColor(Color.Black) 32 | .fontSize(16) 33 | .margin({ right: 15 }) 34 | .fontWeight(400) 35 | .onClick(unDevTip) 36 | } else { 37 | Image(this.subIconSrc) 38 | .width(25) 39 | .height(25) 40 | .margin({ right: 15 }) 41 | .onClick(unDevTip) 42 | } 43 | 44 | } 45 | .height(53) 46 | .width("100%") 47 | .justifyContent(FlexAlign.SpaceBetween) 48 | } 49 | .expandSafeArea([SafeAreaType.KEYBOARD]) 50 | .backgroundColor("#f1f1f1") 51 | .zIndex(999) 52 | .padding({ top: WindowManager.statusBarHeight }) 53 | } 54 | } 55 | 56 | const unDevTip = () => { 57 | Toast.show("功能正在开发中...") 58 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/permissionMananger.ets: -------------------------------------------------------------------------------- 1 | // 导入必要的模块,包括权限管理相关的功能 2 | import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit'; 3 | 4 | export class PermissionManager { 5 | // 静态方法用于检查给定的权限是否已经被授予 6 | static checkPermission(permissions: Permissions[]): boolean { 7 | // 创建一个访问令牌管理器实例 8 | let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); 9 | // 初始化tokenID为0,稍后将获取真实的tokenID 10 | let tokenID: number = 0; 11 | 12 | // 获取本应用的包信息 13 | const bundleInfo = 14 | bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); 15 | 16 | // 设置tokenID为应用的访问令牌ID 17 | tokenID = bundleInfo.appInfo.accessTokenId; 18 | 19 | // 如果没有传入任何权限,则返回false表示没有权限 20 | if (permissions.length === 0) { 21 | return false; 22 | } else { 23 | // 检查所有请求的权限是否都被授予 24 | return permissions.every(permission => 25 | abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED === 26 | atManager.checkAccessTokenSync(tokenID, permission) 27 | ); 28 | } 29 | } 30 | 31 | // 异步静态方法用于请求用户授权指定的权限 32 | static async requestPermission(permissions: Permissions[]): Promise { 33 | // 创建一个访问令牌管理器实例 34 | let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); 35 | 36 | // 获取上下文(这里假设getContext是一个可以获取到UI能力上下文的方法) 37 | let context: Context = getContext() as common.UIAbilityContext; 38 | 39 | // 请求用户授权指定的权限 40 | const result = await atManager.requestPermissionsFromUser(context, permissions); 41 | 42 | // 检查请求结果是否成功(authResults数组中每个元素都应该是0,表示成功) 43 | return !!result.authResults.length && result.authResults.every(authResults => authResults === 0); 44 | } 45 | } -------------------------------------------------------------------------------- /entry/src/main/ets/entryability/EntryAbility.ets: -------------------------------------------------------------------------------- 1 | import UIAbility from '@ohos.app.ability.UIAbility'; 2 | import hilog from '@ohos.hilog'; 3 | import window from '@ohos.window'; 4 | import WindowManager from '../utils/WindowManager'; 5 | import AbilityConstant from '@ohos.app.ability.AbilityConstant'; 6 | import Want from '@ohos.app.ability.Want'; 7 | 8 | export default class EntryAbility extends UIAbility { 9 | 10 | onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 11 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); 12 | } 13 | 14 | onDestroy() { 15 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); 16 | } 17 | 18 | onWindowStageCreate(windowStage: window.WindowStage) { 19 | WindowManager.immerseFullScreenSync(windowStage) 20 | // Main window is created, set main page for this ability 21 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); 22 | 23 | windowStage.loadContent('pages/Index', (err, data) => { 24 | if (err.code) { 25 | hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); 26 | return; 27 | } 28 | hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); 29 | }); 30 | } 31 | 32 | onWindowStageDestroy() { 33 | // Main window is destroyed, release UI related resources 34 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); 35 | } 36 | 37 | onForeground() { 38 | // Ability has brought to foreground 39 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); 40 | } 41 | 42 | onBackground() { 43 | // Ability has back to background 44 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/test/Ability.test.ets: -------------------------------------------------------------------------------- 1 | import hilog from '@ohos.hilog'; 2 | import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from '@ohos/hypium'; 3 | 4 | export default function abilityTest() { 5 | describe('ActsAbilityTest', function () { 6 | // Defines a test suite. Two parameters are supported: test suite name and test suite function. 7 | beforeAll(function () { 8 | // Presets an action, which is performed only once before all test cases of the test suite start. 9 | // This API supports only one parameter: preset action function. 10 | }) 11 | beforeEach(function () { 12 | // Presets an action, which is performed before each unit test case starts. 13 | // The number of execution times is the same as the number of test cases defined by **it**. 14 | // This API supports only one parameter: preset action function. 15 | }) 16 | afterEach(function () { 17 | // Presets a clear action, which is performed after each unit test case ends. 18 | // The number of execution times is the same as the number of test cases defined by **it**. 19 | // This API supports only one parameter: clear action function. 20 | }) 21 | afterAll(function () { 22 | // Presets a clear action, which is performed after all test cases of the test suite end. 23 | // This API supports only one parameter: clear action function. 24 | }) 25 | it('assertContain',0, function () { 26 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. 27 | hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); 28 | let a = 'abc' 29 | let b = 'b' 30 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions. 31 | expect(a).assertContain(b) 32 | expect(a).assertEqual(a) 33 | }) 34 | }) 35 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/chat/ChatFuntionBar.ets: -------------------------------------------------------------------------------- 1 | @Entry 2 | @Component 3 | @Preview 4 | export default struct ChatFunctionBar { 5 | build() { 6 | Grid() { 7 | GridItem() { 8 | this.createMenu($r("app.media.ic_camera"), "相册") 9 | } 10 | 11 | GridItem() { 12 | this.createMenu($r("app.media.ic_shoot"), "拍摄") 13 | } 14 | 15 | GridItem() { 16 | this.createMenu($r("app.media.ic_voice_call"), "语音通话") 17 | } 18 | 19 | GridItem() { 20 | this.createMenu($r("app.media.ic_location"), "位置") 21 | } 22 | 23 | GridItem() { 24 | this.createMenu($r("app.media.ic_red_pkg"), "红包") 25 | } 26 | GridItem() { 27 | this.createMenu($r("app.media.ic_translation"), "转账") 28 | } 29 | 30 | GridItem() { 31 | Text('位置').fontSize(16) 32 | this.createMenu($r("app.media.ic_voice_input"), "语音输入") 33 | } 34 | 35 | GridItem() { 36 | this.createMenu($r("app.media.ic_my_collection"), "我的收藏") 37 | } 38 | 39 | GridItem() { 40 | this.createMenu($r("app.media.ic_voice"), "群工具") 41 | } 42 | 43 | } 44 | .maxCount(4) 45 | .layoutDirection(GridDirection.Row) 46 | .rowsTemplate('1fr 1fr') 47 | .columnsTemplate('1fr 1fr 1fr 1fr') 48 | .height(250) 49 | .backgroundColor("#f9f9f9") 50 | .padding({ top: 20, bottom: 20 }) 51 | } 52 | 53 | @Builder createMenu(iconSrc: Resource, menuName: string) { 54 | Column() { 55 | Image(iconSrc) 56 | .width(60) 57 | .height(60) 58 | .objectFit(ImageFit.Fill) 59 | .margin({ bottom: 5 }) 60 | .padding(13) 61 | .borderRadius(15) 62 | .backgroundColor(Color.White) 63 | // 文本 64 | Text(menuName) 65 | .fontColor(Color.Gray) 66 | .fontSize(14) 67 | } 68 | .borderRadius(20) 69 | .justifyContent(FlexAlign.Center) 70 | .width(70) 71 | .height(70) 72 | } 73 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/PermissionUtils.ets: -------------------------------------------------------------------------------- 1 | import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit'; 2 | 3 | 4 | /** 5 | * 权限管理工具 6 | * @author www.icheny.cn 7 | */ 8 | export class PermissionUtils { 9 | // 检查是否授权 10 | static check(permissions: Permissions[]): boolean { 11 | // 程序访问控制管理 12 | const atManager = abilityAccessCtrl.createAtManager(); 13 | // 获取 bundle 信息 14 | const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION) 15 | // 提取 tokenID 标识 16 | const tokenID = bundleInfo.appInfo.accessTokenId 17 | // 校验应用是否被授予权限 18 | const authResults = permissions.map((item) => atManager.checkAccessTokenSync(tokenID, item)) 19 | // 返回是否已授权结果 20 | return authResults.every(v => v === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) 21 | } 22 | 23 | // 动态申请授权(首次弹窗申请) 24 | static async request(permissions: Permissions[]): Promise { 25 | if (PermissionUtils.check(permissions)) { 26 | return true 27 | } 28 | // 程序访问控制管理 29 | const atManager = abilityAccessCtrl.createAtManager(); 30 | // 拉起弹框请求用户授权 31 | const grantStatus = await atManager.requestPermissionsFromUser(getContext(), permissions) 32 | // 获取请求权限的结果 33 | const isAuth = grantStatus.authResults.every(v => v === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) 34 | // 返回 Promise 授权结果 35 | return isAuth 36 | } 37 | 38 | // 打开系统设置的权限管理页(处理授权结果) 39 | static openPermissionSettingsPage() { 40 | // 获取上下文 41 | const context = getContext() as common.UIAbilityContext 42 | // 获取包信息 43 | const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION) 44 | // 打开系统设置页 45 | context.startAbility({ 46 | bundleName: 'com.huawei.hmos.settings', 47 | abilityName: 'com.huawei.hmos.settings.MainAbility', 48 | uri: 'application_info_entry', 49 | parameters: { 50 | // 按照包名打开对应设置页 51 | pushParams: bundleInfo.name 52 | } 53 | }) 54 | } 55 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/home/Home.ets: -------------------------------------------------------------------------------- 1 | import router from '@ohos.router' 2 | import ListChatItem, { ChatItemData } from '../../component/ListChatItem' 3 | import WechatToolbar from '../../component/WechatToolbar' 4 | 5 | @Preview 6 | @Component 7 | export default struct Home { 8 | @State chatList: ChatItemData[] = [ 9 | { 10 | headSrc: $r("app.media.ic_user_head2"), 11 | nickname: "老婆", 12 | content: "今晚给你做了好吃的,早点下班哦😊", 13 | time: "17:21" 14 | }, 15 | { 16 | headSrc: $r("app.media.ic_user_head3"), 17 | nickname: "游戏好友", 18 | content: "我今天又拿了五杀,牛逼不?", 19 | time: "15:38" 20 | }, 21 | { headSrc: $r("app.media.ic_user_head4"), 22 | nickname: "同事小李", 23 | content: "领导,报给写好了,请过目", 24 | time: "14:12" 25 | }, 26 | { headSrc: $r("app.media.ic_user_head5"), 27 | nickname: "HR小孙", 28 | content: "这个岗位还需要招两个人", 29 | time: "12:08" 30 | }, 31 | { headSrc: $r("app.media.ic_user_head6"), 32 | nickname: "客户小潘", 33 | content: "这是我们公司的报价,希望您能考虑下", 34 | time: "昨天" }, 35 | { headSrc: $r("app.media.ic_user_head7"), 36 | nickname: "老弟", 37 | content: "好的,哥", 38 | time: "周一" } 39 | ] 40 | 41 | build() { 42 | Column() { 43 | WechatToolbar({ title: "微信(5)" }) 44 | 45 | List() { 46 | ForEach(this.chatList, (item: ChatItemData, index: number) => { 47 | ListItem() { 48 | ListChatItem({ data: item }) 49 | } 50 | .onClick(() => { 51 | router.pushUrl({ url: 'pages/chat/ChatPage', 52 | params: {name:item.nickname} 53 | }) 54 | }) 55 | }) 56 | ListItem() { 57 | Divider().color('#0000').strokeWidth(0) 58 | } 59 | } 60 | .divider({ strokeWidth: 0.8, color: '#f0f0f0', startMargin: 90, endMargin: 0 }) // 每行之间的分界线 61 | .backgroundColor(Color.White) 62 | .width("100%") 63 | .height("100%") 64 | 65 | } 66 | .width("100%") 67 | .height("100%") 68 | .backgroundColor("#f0f0f0") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/contact/DataItem.ets: -------------------------------------------------------------------------------- 1 | export interface ContactItem { 2 | name: string; 3 | head: string| Resource; 4 | } 5 | 6 | export default interface DataItem { 7 | title: string; 8 | contactList: ContactItem[]; 9 | } 10 | 11 | class YourClass { 12 | private dataList: DataItem[] = [ 13 | { 14 | title: '', 15 | contactList: [ 16 | { name: "新的朋友", head: $r("app.media.ic_new_friend") }, 17 | { name: "群聊", head: $r("app.media.ic_group_chat") }, 18 | { name: "标签", head: $r("app.media.ic_tag") }, 19 | { name: "公众号", head: $r("app.media.ic_official_account") } 20 | ] 21 | }, 22 | { 23 | title: 'A', 24 | contactList: [ 25 | { name: "ArdWard", head: $r("app.media.ic_user_head2") }, 26 | { name: "阿联酋", head: $r("app.media.ic_user_head3") }, 27 | { name: "艾森宝", head: $r("app.media.ic_user_head6") } 28 | ] 29 | }, 30 | { 31 | title: 'B', 32 | contactList: [ 33 | { name: "宝贝", head: $r("app.media.ic_user_head2") }, 34 | { name: "博士伦", head: $r("app.media.ic_user_head3") }, 35 | { name: "白雪公主", head: $r("app.media.ic_user_head6") } 36 | ] 37 | }, 38 | { 39 | title: 'C', 40 | contactList: [ 41 | { name: "陈先进", head: $r("app.media.ic_user_head7") }, 42 | { name: "蔡伦", head: $r("app.media.ic_user_head5") }, 43 | { name: "褚卫娟", head: $r("app.media.ic_user_head1") } 44 | ] 45 | }, 46 | { 47 | title: 'D', 48 | contactList: [ 49 | { name: "达芬奇", head: $r("app.media.ic_user_head3") }, 50 | { name: "丁春秋", head: $r("app.media.ic_user_head4") }, 51 | { name: "邓浩", head: $r("app.media.ic_user_head5") } 52 | ] 53 | }, 54 | { 55 | title: 'S', 56 | contactList: [ 57 | { name: "石破天", head: $r("app.media.ic_user_head3") }, 58 | { name: "宋浩杰", head: $r("app.media.ic_user_head5") } 59 | ] 60 | }, 61 | { 62 | title: 'Z', 63 | contactList: [ 64 | { name: "猪八戒", head: $r("app.media.ic_user_head3") }, 65 | { name: "张宇恒", head: $r("app.media.ic_user_head4") }, 66 | { name: "周大年", head: $r("app.media.ic_user_head5") } 67 | ] 68 | } 69 | ]; 70 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/testability/TestAbility.ets: -------------------------------------------------------------------------------- 1 | import UIAbility from '@ohos.app.ability.UIAbility'; 2 | import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; 3 | import hilog from '@ohos.hilog'; 4 | import { Hypium } from '@ohos/hypium'; 5 | import testsuite from '../test/List.test'; 6 | import window from '@ohos.window'; 7 | 8 | export default class TestAbility extends UIAbility { 9 | onCreate(want, launchParam) { 10 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); 11 | hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); 12 | hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:'+ JSON.stringify(launchParam) ?? ''); 13 | var abilityDelegator: any 14 | abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() 15 | var abilityDelegatorArguments: any 16 | abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() 17 | hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); 18 | Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite) 19 | } 20 | 21 | onDestroy() { 22 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); 23 | } 24 | 25 | onWindowStageCreate(windowStage: window.WindowStage) { 26 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); 27 | windowStage.loadContent('testability/pages/Index', (err, data) => { 28 | if (err.code) { 29 | hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); 30 | return; 31 | } 32 | hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', 33 | JSON.stringify(data) ?? ''); 34 | }); 35 | } 36 | 37 | onWindowStageDestroy() { 38 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); 39 | } 40 | 41 | onForeground() { 42 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); 43 | } 44 | 45 | onBackground() { 46 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); 47 | } 48 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts: -------------------------------------------------------------------------------- 1 | import hilog from '@ohos.hilog'; 2 | import TestRunner from '@ohos.application.testRunner'; 3 | import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; 4 | 5 | var abilityDelegator = undefined 6 | var abilityDelegatorArguments = undefined 7 | 8 | async function onAbilityCreateCallback() { 9 | hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); 10 | } 11 | 12 | async function addAbilityMonitorCallback(err: any) { 13 | hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); 14 | } 15 | 16 | export default class OpenHarmonyTestRunner implements TestRunner { 17 | constructor() { 18 | } 19 | 20 | onPrepare() { 21 | hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); 22 | } 23 | 24 | async onRun() { 25 | hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); 26 | abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() 27 | abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() 28 | var testAbilityName = abilityDelegatorArguments.bundleName + '.TestAbility' 29 | let lMonitor = { 30 | abilityName: testAbilityName, 31 | onAbilityCreate: onAbilityCreateCallback, 32 | }; 33 | abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) 34 | var cmd = 'aa start -d 0 -a TestAbility' + ' -b ' + abilityDelegatorArguments.bundleName 35 | var debug = abilityDelegatorArguments.parameters['-D'] 36 | if (debug == 'true') 37 | { 38 | cmd += ' -D' 39 | } 40 | hilog.info(0x0000, 'testTag', 'cmd : %{public}s', cmd); 41 | abilityDelegator.executeShellCommand(cmd, 42 | (err: any, d: any) => { 43 | hilog.info(0x0000, 'testTag', 'executeShellCommand : err : %{public}s', JSON.stringify(err) ?? ''); 44 | hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.stdResult ?? ''); 45 | hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.exitCode ?? ''); 46 | }) 47 | hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); 48 | } 49 | } -------------------------------------------------------------------------------- /entry/src/main/ets/component/ListChatContentLeftItem.ets: -------------------------------------------------------------------------------- 1 | import { ChatContentItemData } from '../pages/chat/ChatContentItemData' 2 | import AudioRendererManager from '../utils/AudioRendererManager' 3 | 4 | @Preview 5 | @Component 6 | export default struct ListChatContentLeftItem { 7 | private data?: ChatContentItemData 8 | 9 | build() { 10 | Row() { 11 | Image(this.data?.headSrc) 12 | .width(48) 13 | .height(48) 14 | .borderRadius(4) 15 | .margin({ left: 20, right: 8 }) 16 | .objectFit(ImageFit.Cover) 17 | 18 | Path() 19 | .commands(`M${vp2px(10)} 0 L0 ${vp2px(7)} ${vp2px(10)} ${vp2px(14)} Z`) 20 | .fill('rgb(255,255,255)') 21 | .margin({ 22 | top: 19 23 | }) 24 | .strokeWidth(0) 25 | 26 | Row() { 27 | if (this.data?.msgType === 0) { 28 | Text(this.data?.msg) 29 | .fontColor(Color.Black) 30 | .fontSize(18) 31 | .fontWeight(500) 32 | .borderRadius(4) 33 | .backgroundColor(Color.White) 34 | .padding({ 35 | left: 14, 36 | right: 14, 37 | top: 10, 38 | bottom: 10 39 | }) 40 | 41 | } else if (this.data?.msgType === 2) { 42 | Row() { 43 | Text(this.data?.msg) 44 | .fontColor(Color.Black) 45 | .fontSize(18) 46 | .fontWeight(500) 47 | 48 | Image(this.data?.img) 49 | .width(28) 50 | .width(28) 51 | .margin({ left: 1 }) 52 | .objectFit(ImageFit.Auto) 53 | } 54 | .padding({ 55 | left: 14, 56 | right: 14, 57 | top: 10, 58 | bottom: 10 59 | }) 60 | .borderRadius(4) 61 | .backgroundColor("#a9ea7a") 62 | .onClick(this.playVoice) 63 | 64 | } else { 65 | Image(this.data?.img) 66 | .width(150) 67 | .borderRadius(4) 68 | .objectFit(ImageFit.Auto) 69 | .backgroundColor(Color.White) 70 | } 71 | } 72 | .margin({ right: 80 }) 73 | .layoutWeight(1) 74 | 75 | }.alignItems(VerticalAlign.Top) 76 | } 77 | 78 | playVoice = async () => { 79 | if (this.data?.voicePath) { 80 | AudioRendererManager.doPlay(this.data?.voicePath) 81 | } 82 | } 83 | 84 | aboutToDisappear(): void { 85 | AudioRendererManager.release() 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /entry/src/main/ets/component/ListChatContentRightItem.ets: -------------------------------------------------------------------------------- 1 | import { ChatContentItemData } from '../pages/chat/ChatContentItemData' 2 | import AudioRendererManager from '../utils/AudioRendererManager' 3 | 4 | @Preview 5 | @Component 6 | export default struct ListChatContentRightItem { 7 | data?: ChatContentItemData 8 | 9 | build() { 10 | Row() { 11 | Column() { 12 | if (this.data?.msgType == 0) { 13 | Text(this.data?.msg) 14 | .fontColor(Color.Black) 15 | .fontWeight(500) 16 | .fontSize(18) 17 | .padding({ 18 | left: 14, 19 | right: 14, 20 | top: 10, 21 | bottom: 10 22 | }) 23 | .borderRadius(4) 24 | .backgroundColor("#a9ea7a") 25 | } else if (this.data?.msgType === 2) { 26 | Row() { 27 | Text(this.data?.msg) 28 | .fontColor(Color.Black) 29 | .fontSize(18) 30 | .fontWeight(500) 31 | 32 | Image(this.data?.img) 33 | .width(28) 34 | .width(28) 35 | .margin({ left: 1 }) 36 | .objectFit(ImageFit.Auto) 37 | } 38 | .padding({ 39 | left: 14, 40 | right: 14, 41 | top: 10, 42 | bottom: 10 43 | }) 44 | .borderRadius(4) 45 | .backgroundColor("#a9ea7a") 46 | .onClick(this.playVoice) 47 | 48 | } else { 49 | Image(this.data?.img) 50 | .width(150) 51 | .borderRadius(4) 52 | .objectFit(ImageFit.Auto) 53 | .backgroundColor("#a9ea7a") 54 | } 55 | } 56 | .margin({ left: 80 }) 57 | .alignItems(HorizontalAlign.End) 58 | .layoutWeight(1) 59 | 60 | Path() 61 | .commands(`M0 0 L${vp2px(10)} ${vp2px(7)} 0 ${vp2px(14)} Z`) 62 | .fill('rgb(169,234,122)') 63 | .margin({ 64 | top: 18 65 | }) 66 | .strokeWidth(0) 67 | 68 | Image(this.data?.headSrc) 69 | .width(48) 70 | .height(48) 71 | .borderRadius(4) 72 | .margin({ left: 8, right: 20 }) 73 | .objectFit(ImageFit.Cover) 74 | 75 | }.alignItems(VerticalAlign.Top) 76 | } 77 | 78 | playVoice = async () => { 79 | if (this.data?.voicePath) { 80 | AudioRendererManager.doPlay(this.data?.voicePath) 81 | } 82 | } 83 | 84 | aboutToDisappear(): void { 85 | AudioRendererManager.release() 86 | } 87 | } 88 | 89 | 90 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/qrcode/MyQrCodePage.ets: -------------------------------------------------------------------------------- 1 | import Toolbar from '../../component/Toolbar' 2 | 3 | @Entry 4 | @Component 5 | struct MyQrCodePage { 6 | private value: string = 'https://github.com/ausboyue/Wechat_HarmonyOS' 7 | 8 | build() { 9 | 10 | Column() { 11 | Toolbar({ title: "" }) 12 | Row() { 13 | Image($r('app.media.ic_user_head1')) 14 | .width(55) 15 | .height(55) 16 | .margin({ right: 10 }) 17 | .borderRadius(6) 18 | 19 | Column() { 20 | Text("亚瑟王") 21 | .fontColor(Color.Black) 22 | .fontSize(19) 23 | .fontWeight(500) 24 | 25 | Text("上海 浦东") 26 | .fontColor(Color.Grey) 27 | .fontSize(15) 28 | .margin({ top: 10 }) 29 | } 30 | .alignItems(HorizontalAlign.Start) 31 | 32 | } 33 | .width(280) 34 | .margin({ top: 100 }) 35 | 36 | Stack() { 37 | QRCode(this.value) 38 | .width(280) 39 | .height(280) 40 | .color(Color.Black) 41 | .backgroundColor(Color.White) 42 | .margin({ top: 30 }) 43 | Image($r('app.media.ic_wechat_qrcode_logo')) 44 | .width(60) 45 | .height(60) 46 | .padding(6) 47 | .backgroundColor(Color.White) 48 | .margin({ right: 10 }) 49 | .borderRadius(6) 50 | } 51 | 52 | 53 | Text("扫一扫上面的二维码图案,加我为好友") 54 | .fontColor(Color.Grey) 55 | .fontSize(15) 56 | .margin({ top: 20 }) 57 | 58 | Blank().layoutWeight(1) 59 | Row() { 60 | Text("扫一扫") 61 | .fontColor(Color.Grey) 62 | .fontSize(16) 63 | .fontWeight(600) 64 | .fontColor("#5f6c8a") 65 | .textAlign(TextAlign.Center) 66 | .layoutWeight(1) 67 | 68 | Divider().vertical(true).color(Color.Grey) 69 | .height(10) 70 | 71 | Text("换个样式") 72 | .fontColor(Color.Grey) 73 | .fontSize(16) 74 | .fontWeight(600) 75 | .fontColor("#5f6c8a") 76 | .textAlign(TextAlign.Center) 77 | .layoutWeight(1) 78 | 79 | Divider().vertical(true).color(Color.Grey) 80 | .height(10) 81 | 82 | Text("保存图片") 83 | .fontColor(Color.Grey) 84 | .fontSize(16) 85 | .fontWeight(600) 86 | .fontColor("#5f6c8a") 87 | .textAlign(TextAlign.Center) 88 | .layoutWeight(1) 89 | } 90 | 91 | .width(280) 92 | .margin({ bottom: 80 }) 93 | 94 | } 95 | .width('100%') 96 | } 97 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/AudioRendererManager.ets: -------------------------------------------------------------------------------- 1 | import { audio } from '@kit.AudioKit'; 2 | import { fileIo } from '@kit.CoreFileKit'; 3 | 4 | 5 | class Options { 6 | offset?: number; 7 | length?: number; 8 | } 9 | 10 | 11 | class AudioRendererManager { 12 | /** 13 | * 音频播放实例 14 | */ 15 | private static audioRender: audio.AudioRenderer | null = null 16 | 17 | /** 执行播放 */ 18 | static async doPlay(fileName: string) { 19 | await AudioRendererManager.ready(fileName) 20 | AudioRendererManager.start() 21 | } 22 | 23 | /** 24 | * 初始化准备 25 | */ 26 | private static async ready(fileName: string) { 27 | 28 | try { 29 | let bufferSize: number = 0; 30 | let audioStreamInfo: audio.AudioStreamInfo = { 31 | samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 采样率 32 | channels: audio.AudioChannel.CHANNEL_1, // 通道 33 | sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式 34 | encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式 35 | } 36 | let audioRendererInfo: audio.AudioRendererInfo = { 37 | usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型 38 | rendererFlags: 0 // 音频渲染器标志 39 | } 40 | let audioRendererOptions: audio.AudioRendererOptions = { 41 | streamInfo: audioStreamInfo, 42 | rendererInfo: audioRendererInfo 43 | } 44 | let path = getContext().filesDir; 45 | // let filePath = `${path}/${fileName}.wav`; 46 | let file: fileIo.File = fileIo.openSync(fileName, fileIo.OpenMode.READ_ONLY); 47 | const fileSize = fileIo.statSync(file.path).size 48 | let writeDataCallback = (buffer: ArrayBuffer) => { 49 | let options: Options = { 50 | offset: bufferSize, 51 | length: buffer.byteLength 52 | } 53 | fileIo.readSync(file.fd, buffer, options); 54 | bufferSize += buffer.byteLength; 55 | // 自动停止 56 | if (bufferSize >= fileSize) { 57 | AudioRendererManager.stop() 58 | .then(() => { 59 | AudioRendererManager.release() 60 | }) 61 | } 62 | } 63 | AudioRendererManager.audioRender = await audio.createAudioRenderer(audioRendererOptions) 64 | AudioRendererManager.audioRender.on("writeData", writeDataCallback) 65 | } catch (e) { 66 | console.log("e", e.message, e.code) 67 | } 68 | } 69 | 70 | /** 播放 */ 71 | static async start() { 72 | // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染 73 | await AudioRendererManager.audioRender?.start(); 74 | } 75 | 76 | /** 暂停播放*/ 77 | static async pause() { 78 | await AudioRendererManager.audioRender?.pause() 79 | } 80 | 81 | /** 结束播放 */ 82 | static async stop() { 83 | await AudioRendererManager.audioRender?.stop() 84 | } 85 | 86 | /** 释放资源 */ 87 | static async release() { 88 | await AudioRendererManager.audioRender?.release() 89 | } 90 | } 91 | 92 | export default AudioRendererManager -------------------------------------------------------------------------------- /entry/src/main/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "entry", 4 | "type": "entry", 5 | "description": "$string:module_desc", 6 | "mainElement": "EntryAbility", 7 | "deviceTypes": [ 8 | "phone", 9 | "tablet" 10 | ], 11 | "requestPermissions": [ 12 | { 13 | "name": "ohos.permission.INTERNET", 14 | "reason": "$string:net_permission_reason", 15 | "usedScene": { 16 | "abilities": [ 17 | "EntryAbility" 18 | ], 19 | "when": "inuse" 20 | } 21 | }, 22 | { 23 | "name": "ohos.permission.CAMERA", 24 | "reason": "$string:camera_permission_reason", 25 | "usedScene": { 26 | "abilities": [ 27 | "EntryAbility" 28 | ], 29 | "when": "inuse" 30 | } 31 | }, 32 | { 33 | "name": "ohos.permission.READ_MEDIA", 34 | "reason": "$string:read_media_permission_reason", 35 | "usedScene": { 36 | "abilities": [ 37 | "EntryAbility" 38 | ], 39 | "when": "inuse" 40 | } 41 | }, 42 | { 43 | "name": "ohos.permission.WRITE_MEDIA", 44 | "reason": "$string:write_media_permission_reason", 45 | "usedScene": { 46 | "abilities": [ 47 | "EntryAbility" 48 | ], 49 | "when": "inuse" 50 | } 51 | }, 52 | { 53 | "name": "ohos.permission.GET_NETWORK_INFO", 54 | "reason": "$string:net_info_permission_reason", 55 | "usedScene": { 56 | "abilities": [ 57 | "EntryAbility" 58 | ], 59 | "when": "inuse" 60 | } 61 | }, 62 | { 63 | "name": "ohos.permission.VIBRATE", 64 | "usedScene": { 65 | "abilities": [ 66 | "EntryAbility" 67 | ], 68 | "when": "inuse" 69 | } 70 | }, 71 | { 72 | "name": "ohos.permission.MICROPHONE", 73 | "reason": "$string:voice_reason", 74 | "usedScene": { 75 | "abilities": [ 76 | "FormAbility" 77 | ], 78 | "when": "always" 79 | } 80 | } 81 | ], 82 | "deliveryWithInstall": true, 83 | "installationFree": false, 84 | "pages": "$profile:main_pages", 85 | "abilities": [ 86 | { 87 | "name": "EntryAbility", 88 | "srcEntry": "./ets/entryability/EntryAbility.ets", 89 | "description": "$string:EntryAbility_desc", 90 | "icon": "$media:wechat_logo", 91 | "label": "$string:EntryAbility_label", 92 | "startWindowIcon": "$media:icon", 93 | "startWindowBackground": "$color:start_window_background", 94 | "exported": true, 95 | "skills": [ 96 | { 97 | "entities": [ 98 | "entity.system.home" 99 | ], 100 | "actions": [ 101 | "action.system.home" 102 | ] 103 | } 104 | ] 105 | } 106 | ] 107 | } 108 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/Index.ets: -------------------------------------------------------------------------------- 1 | import home from './home/Home' 2 | import contact from './contact/Contact' 3 | import discovery from './discovery/Discovery' 4 | import mine from './mine/Mine' 5 | import WindowManager from '../utils/WindowManager' 6 | 7 | @Entry 8 | @Component 9 | struct Index { 10 | @State private currentIndex: number = 0 11 | private tabController: TabsController = new TabsController() 12 | private tabTitles: string[] = ["微信", "通信录", "发现", "我"] 13 | private tabTextColors: string[] = ["#040404", "#57be6a"] 14 | private tabTipCounts: number[] = [5, 0, 8, 0] 15 | 16 | build() { 17 | Column() { 18 | Tabs({ barPosition: BarPosition.End, controller: this.tabController }) { 19 | // 微信(首页) 20 | TabContent() { 21 | home() 22 | } 23 | .tabBar(this.createTabMenu(0)) 24 | 25 | // 通讯录 26 | TabContent() { 27 | contact() 28 | } 29 | .tabBar(this.createTabMenu(1)) 30 | 31 | // 发现 32 | TabContent() { 33 | discovery() 34 | } 35 | .tabBar(this.createTabMenu(2)) 36 | 37 | // 我的 38 | TabContent() { 39 | mine() 40 | } 41 | .tabBar(this.createTabMenu(3)) 42 | } 43 | .width("100%") 44 | .height("100%") 45 | .barHeight(65) 46 | .backgroundColor("#f4f4f4") 47 | .vertical(false) 48 | .layoutWeight(1) 49 | .padding({ bottom: WindowManager.navBarHeight }) 50 | .onChange((index: number) => { 51 | this.currentIndex = index 52 | }) 53 | 54 | } 55 | .width('100%') 56 | .height('100%') 57 | } 58 | 59 | @Builder 60 | createTabMenu(index: number) { 61 | 62 | Stack({ alignContent: Alignment.Center }) { 63 | 64 | // 数字右标 65 | Badge({ 66 | count: this.tabTipCounts[index], 67 | maxCount: 99, 68 | position: BadgePosition.RightTop, 69 | style: { 70 | color: 0xFFFFFF, 71 | fontSize: 13, 72 | badgeSize: 20, 73 | badgeColor: Color.Red 74 | } 75 | }) { 76 | Column() { 77 | /** 78 | * Contain 图片按比例缩放以适应容器边界,可能会出现空白区域 79 | Cover 图片按比例缩放以填充容器,可能会被裁剪 80 | Auto 默认值,图片大小保持原始大小 81 | Fill 图片拉伸以填充容器,可能会出现失真 82 | ScaleDown 图片按比例缩小以适应容器边界,但是不会放大图片 83 | None 不进行任何缩放操作,即使图片溢出容器也不会被裁剪或缩放 84 | */ 85 | // icon图标 86 | Image(this.currentIndex === index ? `/images/main_page_index${index}_pre.png` : 87 | `/images/main_page_index${index}_nor.png`) 88 | .width(29.714) 89 | .height(26) 90 | .objectFit(ImageFit.Fill) 91 | .margin({ bottom: 3 }) 92 | // 文本 93 | Text(this.tabTitles[index]) 94 | .fontColor(this.tabTextColors[this.currentIndex === index?1:0]) 95 | .fontSize(14) 96 | } 97 | .backgroundColor(Color.Transparent) 98 | .justifyContent(FlexAlign.Center) 99 | } 100 | .width(42) 101 | .height(40) 102 | } 103 | .width("100%") 104 | .height("100%") 105 | } 106 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/WindowManager.ets: -------------------------------------------------------------------------------- 1 | import window from '@ohos.window'; 2 | 3 | export default class WindowManager { 4 | static statusBarHeight: number = 0 5 | static navBarHeight: number = 0 6 | 7 | /** 8 | * 沉浸式状态栏颜色和状态栏icon 9 | * 一般推荐这个方案,但这个目前仅是api先支持,部分系统还未跟进支持(目前大多数设备无效) 10 | * 在Ability或Component中使用都可 11 | */ 12 | static async immerseColor(color?: string, light?: boolean) { 13 | // 获取当前应用窗口 14 | let windowClass: window.Window = await window.getLastWindow(getContext()) 15 | // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 16 | await windowClass.setWindowSystemBarProperties({ 17 | navigationBarColor: color, 18 | statusBarColor: color, 19 | navigationBarContentColor: color, 20 | statusBarContentColor: color, 21 | isStatusBarLightIcon: light, 22 | isNavigationBarLightIcon: light 23 | }) 24 | } 25 | 26 | /** 27 | * 经测试,异步WindowManager.statusBarHeight,WindowManager.navBarHeight也可以及时赋上值 28 | * ,如果各位项目遇到未拿到值得情况可以使用immerseFullScreenSync同步方法,同步方法也没啥性能和卡顿损耗 29 | * 沉浸式全屏(全屏屏幕,且显示状态栏、导航栏) 30 | * 异步方法:适合动态监听状态栏,导航栏并做相应调整的方式。这种方案比较麻烦,但是用户体验和性能更好。 31 | * 仅在Ability使用(Ability全局,且初始化状态栏和导航栏的高度),建议在 Ability --> onWindowStageCreate 中执行 32 | */ 33 | static async immerseFullScreenAsync(windowStage: window.WindowStage) { 34 | let windowClass: window.Window = await windowStage.getMainWindow() 35 | // 获取状态栏和导航栏的高度 36 | windowClass.on("avoidAreaChange", async data => { 37 | if (data.type == window.AvoidAreaType.TYPE_SYSTEM) { 38 | // 将值保存到静态变量中,以便后续使用 39 | WindowManager.statusBarHeight = px2vp(data.area.topRect.height) 40 | WindowManager.navBarHeight = px2vp(data.area.bottomRect.height) 41 | } 42 | if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) { 43 | // 这里直接覆盖底部三键导航栏的高度,因为两者显示是互斥的 44 | WindowManager.navBarHeight = px2vp(data.area.bottomRect.height) 45 | } 46 | }) 47 | // 设置窗口布局为沉浸式布局 48 | windowClass.setWindowLayoutFullScreen(true) 49 | // 设置状态栏和导航栏可用 50 | windowClass.setWindowSystemBarEnable(["status", "navigation"]) 51 | // 设置状态栏和导航栏的背景为透明 52 | windowClass.setWindowSystemBarProperties({ 53 | navigationBarColor: "#00000000", 54 | statusBarColor: "#00000000", 55 | // navigationBarContentColor: "#FF000000", 56 | // statusBarContentColor: "#FF000000" 57 | }) 58 | } 59 | 60 | /** 61 | * 沉浸式全屏(全屏屏幕,且显示状态栏、导航栏) 62 | * 同步方法:适合一次性定性设置沉浸式状态栏,不会动态调整。此方案大多数场景够用了,用户不会频繁显示|隐藏导航栏。 63 | * 仅在Ability使用(Ability全局,且初始化状态栏和导航栏的高度),建议在 Ability --> onWindowStageCreate 中执行 64 | */ 65 | static immerseFullScreenSync(windowStage: window.WindowStage) { 66 | // 同步获取一个主窗口实例 67 | let windowClass: window.Window = windowStage.getMainWindowSync() 68 | let area = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); 69 | let navBar = px2vp(area.bottomRect.height) 70 | area = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); 71 | // 状态栏高度 72 | const statusBar = px2vp(area.topRect.height) 73 | if (!navBar) { 74 | // 优先获取底部指示器导航栏高度,如果没有值得再尝试获取常规导航栏(鸿蒙5.0以下的传统底部三键导航栏)高度,两者显示是互斥的 75 | navBar = px2vp(area.bottomRect.height) 76 | } 77 | 78 | // 将值保存到静态变量中,以便后续使用 79 | WindowManager.statusBarHeight = statusBar 80 | WindowManager.navBarHeight = navBar 81 | 82 | // 设置窗口布局为沉浸式布局 83 | windowClass.setWindowLayoutFullScreen(true) 84 | windowClass.setWindowSystemBarEnable(["status", "navigation"]) 85 | // 设置状态栏和导航栏的背景为透明 86 | windowClass.setWindowSystemBarProperties({ 87 | navigationBarColor: "#00000000", 88 | statusBarColor: "#00000000", 89 | // navigationBarContentColor: "#FF000000", 90 | // statusBarContentColor: "#FF000000" 91 | }) 92 | } 93 | 94 | /** 95 | * 完全全屏(全铺屏幕,且隐藏状态栏、导航栏) 96 | * 在Ability或Component中使用都可 97 | */ 98 | static async fullScreen() { 99 | let windowClass = await window.getLastWindow(getContext()) 100 | //设置导航栏,状态栏不可见 101 | await windowClass.setWindowSystemBarEnable([]) 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /entry/src/main/ets/utils/SpeechRecognizerManager.ets: -------------------------------------------------------------------------------- 1 | import { speechRecognizer } from '@kit.CoreSpeechKit'; 2 | import { fileIo } from '@kit.CoreFileKit'; 3 | 4 | 5 | class SpeechRecognizerManager { 6 | /** 7 | * 语种信息 8 | * 语音模式:长 9 | */ 10 | private static extraParam: Record = { "locate": "CN", "recognizerMode": "long" }; 11 | private static initParamsInfo: speechRecognizer.CreateEngineParams = { 12 | /** 13 | * 地区信息 14 | * */ 15 | language: 'zh-CN', 16 | /** 17 | * 离线模式:1 18 | */ 19 | online: 1, 20 | extraParams: this.extraParam 21 | }; 22 | /** 23 | * 引擎 24 | */ 25 | private static asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null 26 | /** 27 | * 录音结果 28 | */ 29 | static speechResult: speechRecognizer.SpeechRecognitionResult | null = null 30 | /** 31 | * 会话ID 32 | */ 33 | private static sessionId: string = "asr" + Date.now() 34 | 35 | /** 36 | * 创建引擎 37 | */ 38 | private static async createEngine() { 39 | // 设置创建引擎参数 40 | SpeechRecognizerManager.asrEngine = await speechRecognizer.createEngine(SpeechRecognizerManager.initParamsInfo) 41 | } 42 | 43 | /** 44 | * 设置回调 45 | */ 46 | private static setListener(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => { 47 | }) { 48 | // 创建回调对象 49 | let setListener: speechRecognizer.RecognitionListener = { 50 | // 开始识别成功回调 51 | onStart(sessionId: string, eventMessage: string) { 52 | }, 53 | // 事件回调 54 | onEvent(sessionId: string, eventCode: number, eventMessage: string) { 55 | }, 56 | // 识别结果回调,包括中间结果和最终结果 57 | onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) { 58 | SpeechRecognizerManager.speechResult = result 59 | callback && callback(result) 60 | }, 61 | // 识别完成回调 62 | onComplete(sessionId: string, eventMessage: string) { 63 | }, 64 | // 错误回调,错误码通过本方法返回 65 | // 如:返回错误码1002200006,识别引擎正忙,引擎正在识别中 66 | // 更多错误码请参考错误码参考 67 | onError(sessionId: string, errorCode: number, errorMessage: string) { 68 | }, 69 | } 70 | // 设置回调 71 | SpeechRecognizerManager.asrEngine?.setListener(setListener); 72 | } 73 | 74 | /** 75 | * 开始监听 76 | * */ 77 | static startListening() { 78 | // 设置开始识别的相关参数 79 | let recognizerParams: speechRecognizer.StartParams = { 80 | // 会话id 81 | sessionId: SpeechRecognizerManager.sessionId, 82 | // 音频配置信息。 83 | audioInfo: { 84 | // 音频类型。 当前仅支持“pcm” 85 | audioType: 'pcm', 86 | // 音频的采样率。 当前仅支持16000采样率 87 | sampleRate: 16000, 88 | // 音频返回的通道数信息。 当前仅支持通道1。 89 | soundChannel: 1, 90 | // 音频返回的采样位数。 当前仅支持16位 91 | sampleBit: 16 92 | }, 93 | // 录音识别 94 | extraParams: { 95 | // 0 实时录音识别 96 | "recognitionMode": 0, 97 | // 最大支持音频时长 98 | maxAudioDuration: 60000 99 | } 100 | } 101 | // 调用开始识别方法 102 | SpeechRecognizerManager.asrEngine?.startListening(recognizerParams); 103 | }; 104 | 105 | /** 106 | * 取消识别 107 | */ 108 | static cancel() { 109 | SpeechRecognizerManager.asrEngine?.cancel(SpeechRecognizerManager.sessionId) 110 | } 111 | 112 | /** 113 | * 结束识别 114 | */ 115 | static finish() { 116 | SpeechRecognizerManager.asrEngine?.finish(SpeechRecognizerManager.sessionId) 117 | } 118 | 119 | /** 120 | * 释放ai语音转文字引擎 121 | */ 122 | static shutDown() { 123 | SpeechRecognizerManager.asrEngine?.shutdown() 124 | } 125 | 126 | /** 127 | * 停止并且释放资源 128 | */ 129 | static async release() { 130 | SpeechRecognizerManager.cancel() 131 | SpeechRecognizerManager.shutDown() 132 | 133 | } 134 | 135 | /** 136 | * 初始化ai语音转文字引擎 实现一键开启语音识别 137 | */ 138 | static async init(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => { 139 | }) { 140 | await SpeechRecognizerManager.createEngine() 141 | SpeechRecognizerManager.setListener(callback) 142 | SpeechRecognizerManager.startListening() 143 | } 144 | } 145 | 146 | export default SpeechRecognizerManager -------------------------------------------------------------------------------- /entry/src/main/ets/utils/AudioCapturerManager.ets: -------------------------------------------------------------------------------- 1 | // 导入音频处理模块 2 | import { audio } from '@kit.AudioKit'; 3 | // 导入文件系统模块 4 | import fs from '@ohos.file.fs'; 5 | 6 | // 定义一个接口来描述录音文件的信息 7 | export interface RecordFile { 8 | recordFilePath: string, // 录音文件的路径 9 | startRecordTime: number, // 开始录音的时间戳 10 | endRecordTime: number // 结束录音的时间戳 11 | } 12 | 13 | // 定义一个管理音频录制的类 14 | export class AudioCaptureManager { 15 | // 静态属性,用于存储当前的音频捕获器实例 16 | static audioCapturer: audio.AudioCapturer | null = null; 17 | // 静态私有属性,用于存储录音文件的路径 18 | private static recordFilePath: string = ""; 19 | // 静态私有属性,用于存储开始录音的时间戳 20 | private static startRecordTime: number = 0; 21 | // 静态私有属性,用于存储结束录音的时间戳 22 | private static endRecordTime: number = 0; 23 | 24 | // 静态异步方法,用于创建音频捕获器实例 25 | static async createAudioCapturer() { 26 | if (AudioCaptureManager.audioCapturer) { 27 | return AudioCaptureManager.audioCapturer 28 | } 29 | // 设置音频流信息配置 30 | let audioStreamInfo: audio.AudioStreamInfo = { 31 | samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 设置采样率为16kHz 32 | channels: audio.AudioChannel.CHANNEL_1, // 设置单声道 33 | sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 设置样本格式为16位小端 34 | encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 设置编码类型为原始数据 35 | }; 36 | 37 | // 设置音频捕获信息配置 38 | let audioCapturerInfo: audio.AudioCapturerInfo = { 39 | source: audio.SourceType.SOURCE_TYPE_MIC, // 设置麦克风为音频来源 40 | capturerFlags: 0 // 捕获器标志,此处为默认值 41 | }; 42 | 43 | // 创建音频捕获选项对象 44 | let audioCapturerOptions: audio.AudioCapturerOptions = { 45 | streamInfo: audioStreamInfo, // 使用上面定义的音频流信息 46 | capturerInfo: audioCapturerInfo // 使用上面定义的音频捕获信息 47 | }; 48 | 49 | // 创建音频捕获器实例 50 | AudioCaptureManager.audioCapturer = await audio.createAudioCapturer(audioCapturerOptions); 51 | 52 | // 返回创建的音频捕获器实例 53 | return AudioCaptureManager.audioCapturer; 54 | } 55 | 56 | // 静态异步方法,用于启动录音过程 57 | static async startRecord(fileName: string) { 58 | await AudioCaptureManager.createAudioCapturer() 59 | // 记录开始录音的时间戳 60 | AudioCaptureManager.startRecordTime = Date.now(); 61 | try { 62 | // 初始化缓冲区大小 63 | let bufferSize: number = 0; 64 | 65 | // 定义一个内部类来设置写入文件时的选项 66 | class Options { 67 | offset?: number; // 文件写入位置偏移量 68 | length?: number; // 写入数据的长度 69 | } 70 | 71 | // 获取应用的文件目录路径 72 | let path = getContext().filesDir; 73 | 74 | // 设置录音文件的完整路径 75 | let filePath = `${path}/${fileName}.wav`; 76 | AudioCaptureManager.recordFilePath = filePath; 77 | 78 | // 打开或创建录音文件 79 | let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); 80 | 81 | // 定义一个读取数据的回调函数 82 | let readDataCallback = (buffer: ArrayBuffer) => { 83 | // 创建一个写入文件的选项对象 84 | let options: Options = { 85 | offset: bufferSize, // 文件当前位置偏移量 86 | length: buffer.byteLength // 数据长度 87 | }; 88 | // 将数据写入文件 89 | fs.writeSync(file.fd, buffer, options); 90 | // 更新缓冲区大小 91 | bufferSize += buffer.byteLength; 92 | }; 93 | 94 | // 给音频捕获器实例注册读取数据的事件监听器 95 | AudioCaptureManager.audioCapturer?.on('readData', readDataCallback); 96 | 97 | // 开始录音 98 | AudioCaptureManager.audioCapturer?.start(); 99 | 100 | // 返回录音文件的路径 101 | return filePath; 102 | } catch (e) { 103 | // 如果出现异常,返回空字符串 104 | return ""; 105 | } 106 | } 107 | 108 | // 静态异步方法,用于停止录音过程 109 | static async stopRecord() { 110 | // 停止音频捕获器的工作 111 | AudioCaptureManager.audioCapturer?.stop(); 112 | // 释放音频捕获器的资源 113 | AudioCaptureManager.audioCapturer?.release(); 114 | // 清除音频捕获器实例 115 | AudioCaptureManager.audioCapturer = null; 116 | // 记录结束录音的时间戳 117 | AudioCaptureManager.endRecordTime = Date.now(); 118 | 119 | // 创建并返回一个包含录音文件信息的对象 120 | const recordFile: RecordFile = { 121 | recordFilePath: AudioCaptureManager.recordFilePath, 122 | startRecordTime: AudioCaptureManager.startRecordTime, 123 | endRecordTime: AudioCaptureManager.endRecordTime, 124 | }; 125 | 126 | // 返回记录文件信息 127 | return recordFile; 128 | } 129 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/mine/Mine.ets: -------------------------------------------------------------------------------- 1 | import router from '@ohos.router' 2 | import ListMenuItem from '../../component/ListMenuItem' 3 | import { PhotoPickerUtils } from '../../utils/PhotoPickerUtils' 4 | import WindowManager from '../../utils/WindowManager' 5 | import { Toast } from '../../utils/Toast' 6 | 7 | @Preview 8 | @Component 9 | export default struct Mine { 10 | @State imgPath: string = "" 11 | 12 | build() { 13 | Column() { 14 | 15 | Row() { 16 | Image(this.imgPath ? this.imgPath : $r('app.media.ic_user_head1')) 17 | .width(70) 18 | .height(70) 19 | .margin({ left: 25, right: 10 }) 20 | .borderRadius(8) 21 | .onClick(() => { 22 | PhotoPickerUtils.openGallery( 23 | uri => { 24 | this.imgPath = uri; 25 | }, errMsg => { 26 | Toast.show(errMsg) 27 | }) 28 | }) 29 | 30 | Column() { 31 | Text("亚瑟王") 32 | .fontColor(Color.Black) 33 | .fontSize(26) 34 | .fontWeight(700) 35 | 36 | Text("微信号:wx_023021") 37 | .fontColor(Color.Grey) 38 | .fontSize(16) 39 | .margin({ top: 15 }) 40 | 41 | Text('+ 状态') 42 | .fontColor(Color.Grey) 43 | .margin({ top: 20 }) 44 | .padding({ left: 8, right: 8 }) 45 | .height(25) 46 | .fontSize(15) 47 | .borderRadius(20) 48 | .height(26) 49 | .textAlign(TextAlign.Center) 50 | .borderWidth(0.7) 51 | .borderColor('#666') 52 | .backgroundColor(Color.White) 53 | } 54 | .margin({ top: 40 }) 55 | .alignItems(HorizontalAlign.Start) 56 | 57 | Blank() 58 | Image($r('app.media.ic_qrcode')) 59 | .width(16) 60 | .height(16) 61 | .margin({ top: 38, right: 20 }) 62 | 63 | 64 | Image($r("app.media.ic_more")) 65 | .width(9) 66 | .height(16.364) 67 | .margin({ top: 38, right: 15 }) 68 | 69 | }.width("100%") 70 | .height(203) 71 | .backgroundColor(Color.White) 72 | .padding({ top: WindowManager.statusBarHeight }) 73 | .onClick(() => { 74 | router.pushUrl({ url: 'pages/qrcode/MyQrCodePage' }) 75 | }) 76 | 77 | Divider() 78 | .vertical(false) 79 | .color("#f3f3f3") 80 | .strokeWidth(10) 81 | .lineCap(LineCapStyle.Round) 82 | ListMenuItem({ 83 | title: "服务", 84 | iconSrc: $r('app.media.ic_service') 85 | }) 86 | .onClick(unDevTip) 87 | Divider() 88 | .vertical(false) 89 | .color("#f3f3f3") 90 | .strokeWidth(10) 91 | .lineCap(LineCapStyle.Round) 92 | ListMenuItem({ 93 | title: "收藏", 94 | iconSrc: $r("app.media.ic_collect") 95 | }) 96 | .onClick(unDevTip) 97 | 98 | Divider() 99 | .vertical(false) 100 | .color("#f3f3f3") 101 | .strokeWidth(1) 102 | .lineCap(LineCapStyle.Round) 103 | ListMenuItem({ 104 | title: "朋友圈", 105 | iconSrc: $r('app.media.ic_moment_pic') 106 | }) 107 | .onClick(unDevTip) 108 | Divider() 109 | .vertical(false) 110 | .color("#f3f3f3") 111 | .strokeWidth(1) 112 | .lineCap(LineCapStyle.Round) 113 | ListMenuItem({ 114 | title: "卡包", 115 | iconSrc: $r('app.media.ic_card_pkg') 116 | }).onClick(unDevTip) 117 | Divider() 118 | .vertical(false) 119 | .color("#f3f3f3") 120 | .strokeWidth(1) 121 | .lineCap(LineCapStyle.Round) 122 | ListMenuItem({ 123 | title: "表情", 124 | iconSrc: $r('app.media.ic_face_icon') 125 | }).onClick(unDevTip) 126 | Divider() 127 | .vertical(false) 128 | .color("#f3f3f3") 129 | .strokeWidth(10) 130 | .lineCap(LineCapStyle.Round) 131 | ListMenuItem({ 132 | title: "设置", 133 | iconSrc: $r('app.media.ic_setting') 134 | }) 135 | .onClick(unDevTip) 136 | } 137 | .width("100%") 138 | .height("100%") 139 | .backgroundColor("#f0f0f0") 140 | 141 | } 142 | } 143 | 144 | const unDevTip = () => { 145 | Toast.show("二级页面正在开发中...") 146 | } 147 | -------------------------------------------------------------------------------- /entry/src/main/ets/component/SoundRecordComponent.ets: -------------------------------------------------------------------------------- 1 | 2 | @Component 3 | export struct SoundRecordComponent{ 4 | build() { 5 | // Column() { 6 | // // 1 中心的提示 显示波浪线 7 | // Row() { 8 | // if (this.pressCancelVoicePostText !== PressCancelVoicePostText.postText) { 9 | // // 声纹 10 | // this.vocalPrint() 11 | // 12 | // } else { 13 | // Scroll() { 14 | // // 显示录音的文字 15 | // Text(this.voiceToText) 16 | // .fontSize(12) 17 | // .fontColor("#666") 18 | // } 19 | // .width("100%") 20 | // .height("100%") 21 | // } 22 | // 23 | // Text() 24 | // .width(10) 25 | // .height(10) 26 | // .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.Red : 27 | // "#95EC6A") 28 | // .position({ 29 | // bottom: -5, 30 | // left: "50%" 31 | // }) 32 | // .translate({ 33 | // x: "-50%" 34 | // }) 35 | // .rotate({ 36 | // angle: 45 37 | // }) 38 | // } 39 | // .width("50%") 40 | // .height(80) 41 | // .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.Red : "#95EC6A") 42 | // .position({ 43 | // top: "40%", 44 | // left: "50%" 45 | // }) 46 | // .translate({ 47 | // x: "-50%" 48 | // }) 49 | // .borderRadius(10) 50 | // .justifyContent(FlexAlign.Center) 51 | // .alignItems(VerticalAlign.Center) 52 | // 53 | // // 2 取消和转文字 54 | // Row() { 55 | // Row() { 56 | // Text("X") 57 | // .fontSize(20) 58 | // .width(60) 59 | // .height(60) 60 | // .borderRadius(30) 61 | // .fontColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#000" : "#ccc") 62 | // .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#fff" : "#333") 63 | // .textAlign(TextAlign.Center) 64 | // .align(Alignment.Center) 65 | // .fontColor("#ccc") 66 | // .id("aabb") 67 | // .rotate({ angle: -20 }) 68 | // .onAppear(() => { 69 | // let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById("aabb"); 70 | // this.xScreenOffset.x = px2vp(modePosition.screenOffset.x) 71 | // this.xScreenOffset.y = px2vp(modePosition.screenOffset.y) 72 | // this.xScreenOffset.width = px2vp(modePosition.size.width) 73 | // this.xScreenOffset.height = px2vp(modePosition.size.height) 74 | // }) 75 | // } 76 | // 77 | // Row() { 78 | // Text("文") 79 | // .fontSize(20) 80 | // .width(60) 81 | // .height(60) 82 | // .borderRadius(30) 83 | // .fontColor(this.pressCancelVoicePostText === PressCancelVoicePostText.postText ? "#000" : "#ccc") 84 | // .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.postText ? "#fff" : "#333") 85 | // .textAlign(TextAlign.Center) 86 | // .align(Alignment.Center) 87 | // .id("ddee") 88 | // .rotate({ angle: 20 }) 89 | // .onAppear(() => { 90 | // let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById("ddee"); 91 | // // px单位 92 | // this.TextScreenOffset.x = px2vp(modePosition.screenOffset.x) 93 | // this.TextScreenOffset.y = px2vp(modePosition.screenOffset.y) 94 | // this.TextScreenOffset.width = px2vp(modePosition.size.width) 95 | // this.TextScreenOffset.height = px2vp(modePosition.size.height) 96 | // }) 97 | // 98 | // } 99 | // 100 | // // 3 松开发送 101 | // Text(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? '取消发送' : 102 | // (this.pressCancelVoicePostText === PressCancelVoicePostText.postText ? '转换文字' : "松开发送")) 103 | // .fontColor("#fff") 104 | // .width("100%") 105 | // .position({ 106 | // bottom: 0, 107 | // left: 0 108 | // }) 109 | // .textAlign(TextAlign.Center) 110 | // } 111 | // .width("100%") 112 | // .position({ 113 | // bottom: "23%" 114 | // }) 115 | // .justifyContent(FlexAlign.SpaceBetween) 116 | // .padding({ 117 | // left: 60, right: 60 118 | // }) 119 | // 120 | // // 4 底部白色大球 121 | // Row() { 122 | // 123 | // } 124 | // .width(600) 125 | // .height(600) 126 | // .backgroundColor("#fff") 127 | // .position({ 128 | // bottom: 0, 129 | // left: "50%" 130 | // }) 131 | // .translate({ 132 | // x: "-50%", 133 | // y: "70%" 134 | // }) 135 | // .borderRadius("50%") 136 | // 137 | // } 138 | // .width("100%") 139 | // .height("100%") 140 | // .backgroundColor("rgba(0,0,0,0.5)") 141 | } 142 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/PhotoPickerUtils.ets: -------------------------------------------------------------------------------- 1 | import picker from '@ohos.file.picker'; 2 | import fs from '@ohos.file.fs'; 3 | import { BusinessError } from '@kit.BasicServicesKit'; 4 | import { camera, cameraPicker } from '@kit.CameraKit'; 5 | import { PermissionUtils } from './PermissionUtils'; 6 | 7 | /** 8 | * 相机相册访问工具 9 | * @author www.icheny.cn 10 | */ 11 | export class PhotoPickerUtils { 12 | static async openGallery(onSuccess: (uri: string) => void, onFailed: (error: string) => void) { 13 | try { 14 | 15 | const granted = await PermissionUtils.request(['ohos.permission.WRITE_MEDIA', 'ohos.permission.READ_MEDIA']) 16 | if (!granted) { 17 | setTimeout(() => { 18 | AlertDialog.show({ 19 | title: '温馨提示', 20 | message: '您已拒绝授予存储权限,是否允许前往设置中心授予权限?', 21 | autoCancel: false, 22 | alignment: DialogAlignment.Center, 23 | primaryButton: { 24 | value: '拒绝', 25 | action: () => { 26 | onFailed("用户拒绝授予存储权限"); 27 | } 28 | }, 29 | secondaryButton: { 30 | fontColor: Color.Red, 31 | value: '允许', 32 | action: () => { 33 | PermissionUtils.openPermissionSettingsPage() 34 | onFailed("用户需要前往设置中心授予存储权限"); 35 | } 36 | } 37 | }) 38 | }, 100) 39 | return 40 | } 41 | let PhotoSelectOptions = new picker.PhotoSelectOptions() 42 | PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE 43 | PhotoSelectOptions.maxSelectNumber = 1 44 | let photoPicker = new picker.PhotoViewPicker() 45 | photoPicker.select(PhotoSelectOptions) 46 | .then(async res => { 47 | if (res.photoUris.length < 1) { 48 | onFailed("用户取消") 49 | return 50 | } 51 | const uri = res.photoUris[0] 52 | const newFile = await PhotoPickerUtils.copyFileToCache(uri) 53 | if (newFile.length < 1) { 54 | onFailed("图片获取失败") 55 | res 56 | } 57 | onSuccess(newFile) 58 | }) 59 | .catch((err: BusinessError) => { 60 | onFailed(err.message) 61 | console.error('PhotoViewPicker.select failed with err: ' + JSON.stringify(err)) 62 | }) 63 | } catch (error) { 64 | let err: BusinessError = error as BusinessError 65 | console.error('PhotoViewPicker failed with err: ' + JSON.stringify(err)) 66 | onFailed(err.message) 67 | } 68 | } 69 | 70 | static async openCamera(onSuccess: (uri: string) => void, onFailed: (error: string) => void) { 71 | try { 72 | const granted = await PermissionUtils.request(['ohos.permission.CAMERA']) 73 | if (!granted) { 74 | setTimeout(() => { 75 | AlertDialog.show({ 76 | title: '温馨提示', 77 | message: '您已拒绝授予相机权限,是否允许前往设置中心授予权限?', 78 | autoCancel: false, 79 | alignment: DialogAlignment.Center, 80 | primaryButton: { 81 | value: '拒绝', 82 | action: () => { 83 | onFailed("用户拒绝授予相机权限"); 84 | } 85 | }, 86 | secondaryButton: { 87 | fontColor: Color.Red, 88 | value: '允许', 89 | action: () => { 90 | PermissionUtils.openPermissionSettingsPage() 91 | onFailed("用户需要前往设置中心授予相机权限"); 92 | } 93 | } 94 | }) 95 | }, 100) 96 | return 97 | } 98 | 99 | let pickerProfile: cameraPicker.PickerProfile = { 100 | cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK 101 | } 102 | let pickerResult: cameraPicker.PickerResult = 103 | await cameraPicker.pick(getContext(), [cameraPicker.PickerMediaType.PHOTO, cameraPicker.PickerMediaType.VIDEO], 104 | pickerProfile) 105 | if (pickerResult) { 106 | if (pickerResult.resultUri) { 107 | onSuccess(pickerResult.resultUri) 108 | } else { 109 | onFailed("用户取消") 110 | } 111 | } else { 112 | onFailed("相机不可用") 113 | } 114 | } catch (error) { 115 | let err = error as BusinessError 116 | console.error(`the pick call failed. error code: ${err.code}`) 117 | onFailed(err.message) 118 | } 119 | } 120 | 121 | /** 122 | * file开头路径文件外部APP不能直接进行访问,需拷贝到APP目录下的沙箱文件中目录才能访问读取 123 | */ 124 | static async copyFileToCache(uri: string): Promise { 125 | try { 126 | const index = uri.lastIndexOf('.') 127 | let fileType = index < 0 ? '.jpg' : uri.substring(index) 128 | if (fileType.length < 1) { 129 | fileType = '.jpg' 130 | } 131 | let dir = getContext().cacheDir + '/pictures' 132 | if (!fs.accessSync(dir)) { 133 | fs.mkdirSync(dir) 134 | } 135 | let newPath = dir + `/${new Date().getTime()}${fileType}` 136 | // 将文件拷贝到应用沙箱目录 137 | const file = fs.openSync(uri, fs.OpenMode.READ_ONLY) 138 | await fs.copyFile(file.fd, newPath) 139 | return `file://${newPath}` 140 | } catch (err) { 141 | console.info(`PhotoViewPicker:open file failed with error message: ${err.message}, error code: ${err.code}`) 142 | } 143 | return "" 144 | } 145 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/contact/Contact.ets: -------------------------------------------------------------------------------- 1 | import prompt from '@ohos.promptAction' 2 | import router from '@ohos.router' 3 | import ListContactItem from '../../component/ListContactItem' 4 | import WechatToolbar from '../../component/WechatToolbar' 5 | import DataItem, { ContactItem } from './DataItem' 6 | 7 | @Preview 8 | @Component 9 | export default struct Contact { 10 | private scroller: Scroller = new Scroller() 11 | private dataList: DataItem[] = [ 12 | { 13 | title: '', 14 | contactList: [ 15 | { name: "新的朋友", head: $r("app.media.ic_new_friend") }, 16 | { name: "群聊", head: $r("app.media.ic_group_chat") }, 17 | { name: "标签", head: $r("app.media.ic_tag") }, 18 | { name: "公众号", head: $r("app.media.ic_official_account") } 19 | ] 20 | }, 21 | { 22 | title: 'A', 23 | contactList: [ 24 | { name: "ArdWard", head: $r("app.media.ic_user_head2") }, 25 | { name: "阿联酋", head: $r("app.media.ic_user_head3") }, 26 | { name: "艾森宝", head: $r("app.media.ic_user_head6") } 27 | ] 28 | }, 29 | { 30 | title: 'B', 31 | contactList: [ 32 | { name: "宝贝", head: $r("app.media.ic_user_head2") }, 33 | { name: "博士伦", head: $r("app.media.ic_user_head3") }, 34 | { name: "白雪公主", head: $r("app.media.ic_user_head6") } 35 | ] 36 | }, 37 | { 38 | title: 'C', 39 | contactList: [ 40 | { name: "陈先进", head: $r("app.media.ic_user_head7") }, 41 | { name: "蔡伦", head: $r("app.media.ic_user_head5") }, 42 | { name: "褚卫娟", head: $r("app.media.ic_user_head1") } 43 | ] 44 | }, 45 | { 46 | title: 'D', 47 | contactList: [ 48 | { name: "达芬奇", head: $r("app.media.ic_user_head3") }, 49 | { name: "丁春秋", head: $r("app.media.ic_user_head4") }, 50 | { name: "邓浩", head: $r("app.media.ic_user_head5") } 51 | ] 52 | }, 53 | { 54 | title: 'S', 55 | contactList: [ 56 | { name: "石破天", head: $r("app.media.ic_user_head3") }, 57 | { name: "宋浩杰", head: $r("app.media.ic_user_head5") } 58 | ] 59 | }, 60 | { 61 | title: 'Z', 62 | contactList: [ 63 | { name: "猪八戒", head: $r("app.media.ic_user_head3") }, 64 | { name: "张宇恒", head: $r("app.media.ic_user_head4") }, 65 | { name: "周大年", head: $r("app.media.ic_user_head5") } 66 | ] 67 | } 68 | ]; 69 | private letterList: string[] = 70 | ['↑', '☆', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 71 | 'V', 'W', 'X', 'Y', 'Z', '#'] 72 | 73 | @Builder 74 | itemHead(title: string) { 75 | Text(title) 76 | .fontSize(14) 77 | .visibility("" === title ? Visibility.None : Visibility.Visible) 78 | .backgroundColor("#EDEDED") 79 | .height(36) 80 | .width("100%") 81 | .padding({ left: 20 }) 82 | } 83 | 84 | build() { 85 | Stack({ alignContent: Alignment.End }) { 86 | 87 | Column() { 88 | // 标题栏 89 | WechatToolbar({ title: "通讯录" }) 90 | // 通讯录列表 91 | List({ scroller: this.scroller }) { 92 | // 遍历循环大组标签 93 | ForEach(this.dataList, (item: DataItem) => { 94 | // 大组标签 95 | ListItemGroup({ header: this.itemHead(item.title) }) { 96 | // 遍历循环大组内的联系人列表 97 | ForEach(item.contactList, (contact: ContactItem) => { 98 | // // 小组联系人列表 99 | ListItem() { 100 | ListContactItem({ head: contact.head, name: contact.name }) 101 | } 102 | .onClick(() => { 103 | router.pushUrl({ 104 | url: 'pages/chat/ChatPage', 105 | params: { name: contact.name } 106 | }) 107 | }) 108 | }) 109 | } 110 | .divider({ 111 | strokeWidth: 0.8, 112 | color: '#f0f0f0', 113 | startMargin: 85, 114 | endMargin: 0 115 | }) // 每行之间的分界线 116 | }) 117 | } 118 | .width('100%') 119 | .height(0) 120 | .layoutWeight(1) 121 | .backgroundColor(Color.White) 122 | } 123 | .width("100%") 124 | .height("100%") 125 | 126 | AlphabetIndexer({ arrayValue: this.letterList, selected: 0 }) 127 | .color(Color.Black) 128 | .font({ size: 14 }) 129 | .selectedFont({ size: 14 }) 130 | .selectedColor(Color.Black) 131 | .selectedBackgroundColor(Color.Transparent) 132 | .usingPopup(true) 133 | .popupColor(Color.Red) 134 | .popupBackground("#57be6a") 135 | .popupFont({ size: 32, weight: FontWeight.Bolder }) 136 | .itemSize(20) 137 | .alignStyle(IndexerAlign.Right) 138 | .margin({ top: 80 }) 139 | .onSelect((index: number) => { 140 | let letter = this.letterList[index] 141 | let target: number = 0 142 | for (const item of this.dataList) { 143 | if (letter === item.title) { 144 | this.scroller.scrollToIndex(target) 145 | prompt.showToast({ message: "" + target }) 146 | break 147 | } 148 | target++ 149 | } 150 | }) 151 | 152 | }.width("100%") 153 | .height("100%") 154 | 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/search/SearchPage.ets: -------------------------------------------------------------------------------- 1 | import prompt from '@ohos.promptAction'; 2 | import router from '@ohos.router'; 3 | import common from '@ohos.app.ability.common'; 4 | import window from '@ohos.window'; 5 | import { Toast } from '../../utils/Toast'; 6 | import WindowManager from '../../utils/WindowManager'; 7 | 8 | @Entry 9 | @Component 10 | export default struct SearchPage { 11 | private context = getContext(this) as common.UIAbilityContext; 12 | private controller: TextInputController = new TextInputController() 13 | private keyboardHeight: number = 0 14 | @State private searchText: string = '' 15 | 16 | async aboutToAppear() { 17 | await window.getLastWindow(getContext(this)).then(currentWindow => { 18 | currentWindow.on('keyboardHeightChange', (height) => { 19 | this.keyboardHeight = height; 20 | }) 21 | }) 22 | } 23 | 24 | onBackPress() { 25 | return this.keyboardHeight > 0 26 | } 27 | 28 | build() { 29 | Column() { 30 | Row() { 31 | Stack() { 32 | TextInput({ text: this.searchText, placeholder: '搜索', controller: this.controller }) 33 | .placeholderFont({ size: 20, weight: 400 }) 34 | .caretColor(Color.Green) 35 | .placeholderColor(Color.Grey) 36 | .width('100%') 37 | .height("100%") 38 | .padding({ left: 40, right: 60 }) 39 | .fontSize(20) 40 | .fontColor(Color.Black) 41 | .borderRadius(8)// 设置stateStyles,解决点击编辑框会变色的问题 42 | .stateStyles({ 43 | normal: {.backgroundColor(Color.White) 44 | }, 45 | pressed: {.backgroundColor(Color.White) 46 | }, 47 | clicked: {.backgroundColor(Color.White) 48 | }, 49 | focused: {.backgroundColor(Color.White) 50 | } 51 | }) 52 | .onChange((value: string) => { 53 | this.searchText = value 54 | }) 55 | .onSubmit(() => { 56 | prompt.showToast({ message: this.searchText }) 57 | }) 58 | 59 | Row() { 60 | Image($r("app.media.ic_search")) 61 | .colorBlend(Color.Grey) 62 | .width(20) 63 | .height(20) 64 | if (this.searchText.length != 0) { 65 | Image($r('app.media.ic_delete')) 66 | .width(20) 67 | .height(20) 68 | .colorBlend(Color.Gray) 69 | .onClick(() => { 70 | this.searchText = "" 71 | }) 72 | } else { 73 | Image($r("app.media.ic_voice")) 74 | .width(23) 75 | .height(23) 76 | } 77 | } 78 | .width("100%") 79 | .padding({ left: 10, right: 10 }) 80 | .hitTestBehavior(HitTestMode.None) // NOTE:父组件不消耗触摸事件 81 | .justifyContent(FlexAlign.SpaceBetween) 82 | 83 | // ,({ placeholder: "搜索", controller: this.controller }) 84 | // .height("100%") 85 | // .textFont({ size: 20, weight: 400 }) 86 | // .placeholderFont({ size: 20, weight: 400 }) 87 | // .backgroundColor(Color.White) 88 | // .placeholderColor(Color.Grey) 89 | // .margin({ left: 20, right: 65 }) 90 | // .alignRules({ 91 | // top: { anchor: "__container__", align: VerticalAlign.Top }, 92 | // left: { anchor: "__container__", align: HorizontalAlign.Start } 93 | // }) 94 | // .id("search") 95 | // .onSubmit((value: string) => { 96 | // if (value !== null && "" !== value) { 97 | // prompt.showToast({ message: value }) 98 | // } 99 | // }) 100 | // .borderRadius(8) 101 | } 102 | .margin({ left: 20 }) 103 | .layoutWeight(1) 104 | 105 | Text("取消") 106 | .fontColor("#5f6c8a") 107 | .width(70) 108 | .height("100%") 109 | .fontWeight(400) 110 | .textAlign(TextAlign.Center) 111 | .fontSize(19)// .alignRules({ 112 | // top: { anchor: "__container__", align: VerticalAlign.Top }, 113 | // right: { anchor: "__container__", align: HorizontalAlign.End } 114 | // }) 115 | // .id("cancel") 116 | .onClick(() => { 117 | if ("1" === router.getLength()) { 118 | // app退出,一般不会走这里 119 | this.context.terminateSelf() 120 | } else { 121 | router.back() 122 | } 123 | }) 124 | } 125 | .width("100%") 126 | .height(40) 127 | .justifyContent(FlexAlign.SpaceBetween) 128 | .margin({ top: 10 }) 129 | 130 | 131 | Text("搜索指定内容") 132 | .fontColor(Color.Gray) 133 | .margin({ left: 25, top: 25 }) 134 | .fontSize(15) 135 | 136 | Divider() 137 | .margin({ left: 25, right: 25, top: 12 }) 138 | .height(0.6) 139 | .color("#e6e6e6") 140 | 141 | Row() { 142 | Text("朋友圈") 143 | .width("25%") 144 | .fontColor("#5f6c8a") 145 | .fontSize(17) 146 | .fontWeight(500) 147 | .onClick(unDevTip) 148 | .textAlign(TextAlign.Center) 149 | 150 | Text("公众号") 151 | .width("25%") 152 | .fontColor("#5f6c8a") 153 | .fontSize(17) 154 | .fontWeight(500) 155 | .textAlign(TextAlign.Center) 156 | .onClick(unDevTip) 157 | 158 | Text("小程序") 159 | .width("25%") 160 | .fontColor("#5f6c8a") 161 | .fontSize(17) 162 | .fontWeight(500) 163 | .textAlign(TextAlign.Center) 164 | .onClick(unDevTip) 165 | 166 | Text("视频号") 167 | .width("25%") 168 | .fontWeight(500) 169 | .fontColor("#5f6c8a") 170 | .fontSize(17) 171 | .textAlign(TextAlign.Center) 172 | .onClick(unDevTip) 173 | } 174 | .margin({ top: 12 }) 175 | 176 | Blank() 177 | 178 | Text("页面设置") 179 | .fontColor("#5f6c8a") 180 | .fontSize(17) 181 | .fontWeight(500) 182 | .onClick(unDevTip) 183 | .alignSelf(ItemAlign.Center) 184 | .margin({ bottom: 80 }) 185 | 186 | } 187 | .width("100%") 188 | .height("100%") 189 | .alignItems(HorizontalAlign.Start) 190 | .backgroundColor("#f3f3f3") 191 | .padding({ top: WindowManager.statusBarHeight, bottom: WindowManager.navBarHeight }) 192 | .onKeyEvent((event: KeyEvent) => { 193 | if (event.type === KeyType.Up) { 194 | prompt.showToast({ 195 | message: "按了返回键" 196 | }); 197 | } 198 | event.stopPropagation() 199 | }) 200 | } 201 | } 202 | 203 | const unDevTip = () => { 204 | Toast.show("功能正在开发中...") 205 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/HardwareIndexPage.ets: -------------------------------------------------------------------------------- 1 | import deviceInfo from '@ohos.deviceInfo'; //设备信息 2 | import { process } from '@kit.ArkTS'; //系统运行时间 3 | import { connection } from '@kit.NetworkKit'; //网络连接 4 | import dayjs from 'dayjs'; 5 | import duration from 'dayjs/plugin/duration'; 6 | import { display } from '@kit.ArkUI'; //屏幕 7 | import { sensor } from '@kit.SensorServiceKit'; //传感器数组Api 8 | import WindowManager from '../utils/WindowManager'; 9 | 10 | dayjs.extend(duration) 11 | 12 | @Entry 13 | @Component 14 | struct HardwareIndexPage { 15 | // 产品名称 16 | @State marketName: string = '-' 17 | // 系统版本 18 | @State osFullName: string = '-' 19 | // 系统软件API版本 20 | @State sdkApiVersion: string = '-' 21 | // 设备类型 22 | @State deviceType: string = '-' 23 | // 获取当前系统已运行的秒数 24 | @State uptime: number = 0 25 | // 网络类型 26 | @State netBearType: string = '-' 27 | // IP 地址 28 | @State IPAddress: string = '0.0.0.0' 29 | // 子网掩码 30 | @State subnetMask: string = '0.0.0.0' 31 | // 广播地址 32 | @State broadcastAddress: string = '0.0.0.0' 33 | // 屏幕分辨率(像素) 34 | @State displayHeight: number = 0 35 | @State displayWidth: number = 0 36 | // 屏幕刷新率(Hz) 37 | @State displayRefreshRate: number = 0 38 | // 像素密度(PPI) 39 | @State displayDensityDPI: number = 0 40 | // 支持的传感器id列表 41 | @State supportSensorIds: number[] = [] 42 | 43 | onPageShow() { 44 | this.getDeviceInfo() 45 | } 46 | 47 | // 获取设备硬件各项信息 48 | getDeviceInfo() { 49 | // 利用@ohos.deviceInfo获取设备信息 50 | this.marketName = deviceInfo.marketName 51 | this.osFullName = deviceInfo.osFullName 52 | this.sdkApiVersion = `${deviceInfo.sdkApiVersion}` 53 | this.deviceType = deviceInfo.deviceType 54 | //利用@kit.ArkTs获取系统运行时间,返回值是毫秒 55 | this.uptime = process.uptime() 56 | // 获取屏幕数据 57 | this.getScreenInfo() 58 | // 传感器获取 59 | this.getSenorIdArr() 60 | // 获取网络状态 61 | // 首先判断是否存在默认连接的网络 62 | const hasDefaultNet = connection.hasDefaultNetSync() 63 | // 没有网络 64 | if (!hasDefaultNet) { 65 | // this.netWorkType = '无网络' 66 | return 67 | } else { 68 | this.getConnectionNetBearType() // 获取网络类型 69 | this.getConnectionProperties() // IP地址 70 | } 71 | } 72 | 73 | // 获取网络类型 74 | getConnectionNetBearType() { 75 | // 1、获取默认连接的网络,defaultNet 76 | const defaultNet = connection.getDefaultNetSync() 77 | // 2、获取defaultNet对应的网络的能力信息(网络额能力集)getNetCapabilitySync() 78 | const netCapabilities = connection.getNetCapabilitiesSync(defaultNet) 79 | // 3、在网络的能力集中获取网络类型NetBearType 80 | if (netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_WIFI)) { 81 | this.netBearType = 'WIFI网络' 82 | } else if (netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR)) { 83 | this.netBearType = '蜂窝网络' 84 | } else if (netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_ETHERNET)) { 85 | // 温馨提示:模拟器为以太网网络(网线) 86 | this.netBearType = '以太网网络' 87 | } 88 | } 89 | 90 | // 获取链路信息--进行获取IP地址 91 | getConnectionProperties() { 92 | // 获取默认网络 93 | const defaultNet = connection.getDefaultNetSync() 94 | // 获取默认网络的链路信息 95 | const connectionProperties = connection.getConnectionPropertiesSync(defaultNet) 96 | // 提取链路信息 97 | const linkAddress = connectionProperties.linkAddresses[0] 98 | if (linkAddress) { 99 | // 获取 IP 地址 100 | this.IPAddress = linkAddress.address.address 101 | // 计算子网掩码(了解) 102 | this.subnetMask = this.calculateSubnetMask(linkAddress.prefixLength) 103 | // 计算广播地址(了解) 104 | this.broadcastAddress = this.calculateBroadcastAddress(this.IPAddress, this.subnetMask) 105 | } 106 | } 107 | 108 | /** 109 | * 计算子网掩码 110 | * @param prefixLength 前缀长度 111 | * @returns 子网掩码字符串 112 | */ 113 | calculateSubnetMask(prefixLength: number): string { 114 | // 计算每个字节的子网掩码部分 115 | let subnetMask = ''; 116 | for (let i = 0; i < 4; i++) { 117 | // 每个字节中的有效位数 118 | const bits = Math.min(prefixLength, 8); 119 | // 计算子网掩码字节的值并添加到结果字符串 120 | subnetMask += (256 - Math.pow(2, 8 - bits)) + '.'; 121 | // 更新剩余的位数 122 | prefixLength -= bits; 123 | } 124 | // 去除末尾的点并返回子网掩码字符串 125 | return subnetMask.slice(0, -1); 126 | } 127 | 128 | /** 129 | * 计算广播地址 130 | * @param ipAddress IP地址字符串,例如 "192.168.2.13" 131 | * @param subnetMask 子网掩码字符串,例如 "255.255.255.0" 132 | * @returns 广播地址字符串 133 | */ 134 | calculateBroadcastAddress(ipAddress: string, subnetMask: string): string { 135 | // 将IP地址字符串转换为数字数组 136 | const ipParts: number[] = ipAddress.split('.') 137 | .map(Number); 138 | // 将子网掩码字符串转换为数字数组 139 | const subnetParts: number[] = subnetMask.split('.') 140 | .map(Number); 141 | // 计算每个字节的广播地址部分 142 | const broadcastParts: number[] = []; 143 | for (let i = 0; i < 4; i++) { 144 | // 计算每个字节的广播地址值并添加到结果数组 145 | broadcastParts.push(ipParts[i] | (255 - subnetParts[i])); 146 | } 147 | // 将结果数组转换为字符串并使用点分隔 148 | return broadcastParts.join('.'); 149 | } 150 | 151 | // 获取屏幕数据 152 | getScreenInfo() { 153 | const ScreenInfo = display.getDefaultDisplaySync() 154 | this.displayHeight = ScreenInfo.height 155 | this.displayWidth = ScreenInfo.width 156 | this.displayRefreshRate = ScreenInfo.refreshRate 157 | this.displayDensityDPI = ScreenInfo.densityDPI 158 | } 159 | 160 | // 获取传感器的数据 161 | async getSenorIdArr() { 162 | let SensorList = await sensor.getSensorList() //获取设备传感器的信息 163 | this.supportSensorIds = SensorList.map(item => item.sensorId) //将设备传感器的ID赋值给supportSensorIds 164 | } 165 | 166 | @Builder 167 | ListTitle(title: string) { 168 | Text(title) 169 | .fontSize(16) 170 | .fontWeight(500) 171 | .fontColor(Color.Black) 172 | .width('100%') 173 | .padding({ 174 | left: 20, 175 | right: 20, 176 | top: 20, 177 | bottom: 10 178 | }) 179 | .backgroundColor(Color.White) 180 | } 181 | 182 | build() { 183 | Navigation() { 184 | // 顶部硬件信息 185 | Column({ space: 20 }) { 186 | Text(this.marketName) 187 | .fontSize(16) 188 | .fontColor(Color.White) 189 | Column({ space: 10 }) { 190 | Text('系统版本:' + this.osFullName) 191 | .fontSize(12) 192 | .fontColor(Color.White) 193 | Text('API 版本:' + this.sdkApiVersion) 194 | .fontSize(12) 195 | .fontColor(Color.White) 196 | Text('设备类型:' + this.deviceType) 197 | .fontSize(12) 198 | .fontColor(Color.White) 199 | } 200 | .alignItems(HorizontalAlign.Start) 201 | } 202 | .width('100%') 203 | .padding({ 204 | left: 20, 205 | right: 20, 206 | top: 30, 207 | bottom: 30 208 | }) 209 | .alignItems(HorizontalAlign.Start) 210 | 211 | // 其他内容 212 | Column() { 213 | List() { 214 | ListItemGroup({ header: this.ListTitle('基本信息') }) { 215 | ListRow({ title: '上次启动', value: dayjs(Date.now() - this.uptime * 1000).format('YYYY-MM-DD HH:mm:ss') }) 216 | ListRow({ title: '运行时间', value: dayjs.duration(this.uptime, 'seconds').format('D天H时mm分') }) 217 | } 218 | 219 | ListItemGroup({ header: this.ListTitle('网络信息') }) { 220 | ListRow({ title: '网络类型', value: this.netBearType }) 221 | ListRow({ title: 'IP地址', value: this.IPAddress }) 222 | ListRow({ title: '子网掩码', value: this.subnetMask }) 223 | ListRow({ title: '广播地址', value: this.broadcastAddress }) 224 | } 225 | 226 | ListItemGroup({ header: this.ListTitle('硬件特性') }) { 227 | // 屏幕信息 228 | ListRow({ title: '屏幕分辨率(像素)', value: `${this.displayHeight}x${this.displayWidth}` }) 229 | ListRow({ title: '屏幕刷新率(Hz)', value: this.displayRefreshRate }) 230 | ListRow({ title: '像素密度(PPI)', value: this.displayDensityDPI }) 231 | } 232 | } 233 | .divider({ strokeWidth: 10 }) 234 | .sticky(StickyStyle.Header) 235 | .height('100%') 236 | .width('100%') 237 | } 238 | .width('100%') 239 | .layoutWeight(1) 240 | .backgroundColor(Color.White) 241 | .borderRadius({ topLeft: 12, topRight: 12 }) 242 | .clip(true) 243 | } 244 | .title("硬件信息") 245 | .titleMode(NavigationTitleMode.Mini) 246 | .mode(NavigationMode.Stack) 247 | .hideBackButton(false) 248 | .padding({ top: WindowManager.statusBarHeight }) 249 | .linearGradient({ 250 | angle: 180, 251 | colors: [["#285aee", 0], ["#69b3f7", 0.4], [Color.White, 0.4]] 252 | }) 253 | } 254 | } 255 | 256 | @Component 257 | struct ListRow { 258 | @Prop title: string = '' 259 | @Prop value: string | number = '' 260 | 261 | build() { 262 | Row() { 263 | Text(this.title) 264 | .fontSize(14) 265 | .fontColor(Color.Black) 266 | Text(this.value.toString()) 267 | .fontSize(12) 268 | .fontColor(Color.Gray) 269 | } 270 | .padding({ left: 20, right: 20 }) 271 | .height(40) 272 | .width('100%') 273 | .justifyContent(FlexAlign.SpaceBetween) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/chat/ChatPage.ets: -------------------------------------------------------------------------------- 1 | import router from '@ohos.router'; 2 | import Toolbar from '../../component/Toolbar'; 3 | import ListChatContentLeftItem from '../../component/ListChatContentLeftItem'; 4 | import ListChatContentRightItem from '../../component/ListChatContentRightItem'; 5 | import window from '@ohos.window'; 6 | import WindowManager from '../../utils/WindowManager'; 7 | import ChatFunctionBar from './ChatFuntionBar'; 8 | import { ChatContentItemData } from './ChatContentItemData'; 9 | import ChatPageParam from './ChatPageParam'; 10 | import { Toast } from '../../utils/Toast'; 11 | import SpeechRecognizerManager from '../../utils/SpeechRecognizerManager'; 12 | import { AudioCaptureManager, RecordFile } from '../../utils/AudioCapturerManager'; 13 | import { componentUtils } from '@kit.ArkUI'; 14 | import { vibrator } from '@kit.SensorServiceKit'; 15 | import { PermissionUtils } from '../../utils/PermissionUtils'; 16 | 17 | enum WXInputType { 18 | /** 19 | * 语音输入 20 | */ 21 | voice = 0, 22 | /** 23 | * 文本输入 24 | */ 25 | keyboard = 1 26 | } 27 | 28 | enum MessageType { 29 | /** 30 | * 声音 31 | */ 32 | voice = 0, 33 | /** 34 | * 文本 35 | */ 36 | text = 1 37 | } 38 | 39 | // 消息 40 | class ChatMessage { 41 | /** 42 | * 消息类型:【录音、文本】 43 | */ 44 | type: MessageType 45 | /** 46 | * 内容 [录音-文件路径,文本-内容] 47 | */ 48 | content: string 49 | /** 50 | * 消息时间 51 | */ 52 | time: string 53 | /** 54 | * 声音的持续时间 单位毫秒 55 | */ 56 | duration?: number 57 | /** 58 | * 录音转的文字 59 | */ 60 | translateText?: string 61 | /** 62 | * 是否显示转好的文字 63 | */ 64 | isShowTranslateText: boolean = false 65 | 66 | constructor(type: MessageType, content: string, duration?: number, translateText?: string) { 67 | this.type = type 68 | this.content = content 69 | const date = new Date() 70 | this.time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` 71 | this.duration = duration 72 | this.translateText = translateText 73 | } 74 | } 75 | 76 | enum PressCancelVoicePostText { 77 | // 没有长按 78 | none = 0, 79 | // 长按 没有选中“取消发送”或者"转语音" 80 | pressing = 1, 81 | // 取消发送 82 | cancelVoice = 2, 83 | // 转文字 84 | postText = 3 85 | } 86 | 87 | /** 88 | * 长按时,手指的坐标 89 | */ 90 | interface ScreenOffset { 91 | x: number 92 | y: number 93 | width: number 94 | height: number 95 | } 96 | 97 | @Entry 98 | @Component 99 | export default struct ChatPage { 100 | private name?: string = "微信" 101 | private controller: TextAreaController = new TextAreaController() 102 | private scroller: Scroller = new Scroller() 103 | @State private chatText: string = '' 104 | private keyboardHeight: number = 0 105 | @State private funcBarHeight: number = 0; 106 | @State private showFuncBar: boolean = false; 107 | @State private dataList: ChatContentItemData[] = [ 108 | { 109 | name: "鲁班", 110 | headSrc: $r("app.media.ic_user_head2"), 111 | msgType: 0, 112 | self: true, 113 | msg: "现在几点了?" 114 | }, 115 | { 116 | name: "王昭君", 117 | headSrc: $r("app.media.ic_user_head3"), 118 | msgType: 0, 119 | self: false, 120 | msg: "10点,怎么了?" 121 | }, 122 | { 123 | name: "鲁班", 124 | headSrc: $r("app.media.ic_user_head2"), 125 | msgType: 1, 126 | self: true, 127 | msg: "", 128 | img: $r("app.media.ic_its_over") 129 | }, 130 | { 131 | name: "鲁班", 132 | headSrc: $r("app.media.ic_user_head2"), 133 | msgType: 0, 134 | self: true, 135 | msg: "我要迟到了!!!", 136 | } 137 | , 138 | { 139 | name: "王昭君", 140 | headSrc: $r("app.media.ic_user_head3"), 141 | msgType: 1, 142 | self: false, 143 | msg: "", 144 | img: $r("app.media.ic_hahaha") 145 | } 146 | , 147 | { 148 | name: "王昭君", 149 | headSrc: $r("app.media.ic_user_head3"), 150 | msgType: 0, 151 | self: false, 152 | msg: "那赶紧粗发吧,哈哈哈哈哈哈哈", 153 | } 154 | , 155 | { 156 | name: "鲁班", 157 | headSrc: $r("app.media.ic_user_head2"), 158 | msgType: 0, 159 | self: true, 160 | msg: "嗯嗯,不说了,这就飞奔过去。。。", 161 | } 162 | ] 163 | // 输入框内容 164 | @State 165 | textValue: string = "" 166 | // 输入状态 语音或者文字 167 | @State 168 | inputType: WXInputType = WXInputType.keyboard 169 | // 语音识别的文字 170 | @State 171 | voiceToText: string = "" 172 | // 消息 173 | @State 174 | chatList: ChatMessage[] = [] 175 | // 按住说话 录音模态 176 | @State 177 | showTalkContainer: boolean = false 178 | // 长按状态 179 | @State 180 | pressCancelVoicePostText: PressCancelVoicePostText = PressCancelVoicePostText.none 181 | //用来配置 CanvasRenderingContext2D 对象的参数,包括是否开启抗锯齿,true表明开启抗锯齿。 182 | settings: RenderingContextSettings = new RenderingContextSettings(true) 183 | //用来创建CanvasRenderingContext2D对象,通过在canvas中调用CanvasRenderingContext2D对象来绘制。 184 | context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 185 | // 声明波纹定时器id 186 | voiceTimeId: number = -1 187 | // 底部录音气泡的坐标 188 | bubbleScreenOffset: ScreenOffset = { 189 | x: 0, 190 | y: 0, 191 | width: 0, 192 | height: 0 193 | } 194 | // 录音文件名称 195 | recordFileName: string = "" 196 | // 发送文本消息 197 | sendTextMessage = () => { 198 | if (!this.textValue.trim()) { 199 | return 200 | } 201 | const chat = new ChatMessage(MessageType.text, this.textValue.trim()) 202 | this.chatList.push(chat) 203 | this.textValue = "" 204 | } 205 | // 按住说话 持续触发 206 | onPressTalk = async (event: TouchEvent) => { 207 | if (event.type === TouchType.Down) { 208 | // 手指按下时触发 209 | this.pressCancelVoicePostText = PressCancelVoicePostText.pressing 210 | // 按下 211 | this.showTalkContainer = true 212 | // 震动反馈 213 | this.startVibration() 214 | 215 | // 开始录音 216 | this.onStartRecord() 217 | 218 | // 实时语音识别 219 | this.onStartSpeechRecognize() 220 | 221 | } else if (event.type === TouchType.Move) { 222 | // 手指移动时持续触发 223 | this.pressCancelVoicePostText = PressCancelVoicePostText.pressing 224 | // 获取当前手指的坐标 225 | const x = event.touches[0].displayX 226 | const y = event.touches[0].displayY 227 | // 判断是否碰到了 “X” 228 | let isTouchX = x <= this.bubbleScreenOffset.width / 2 && y < this.bubbleScreenOffset.y 229 | // 判断是否碰到了 "文" 230 | let isTouchText = x > this.bubbleScreenOffset.width / 2 && y < this.bubbleScreenOffset.y 231 | if (isTouchX) { 232 | // 取消发送 233 | this.pressCancelVoicePostText = PressCancelVoicePostText.cancelVoice 234 | } else if (isTouchText) { 235 | // 转换文字 236 | this.pressCancelVoicePostText = PressCancelVoicePostText.postText 237 | } 238 | } else if (event.type === TouchType.Up) { 239 | // 松开手 240 | this.showTalkContainer = false 241 | // 停止录音 res 为录音文件信息,包括录音路径、时长等信息 242 | const res = await this.stopRecord() 243 | // 结束AI语音识别 244 | SpeechRecognizerManager.release() 245 | 246 | if (this.pressCancelVoicePostText === PressCancelVoicePostText.postText) { 247 | // 转换文字 248 | this.postText() 249 | } else if (this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice) { 250 | // 取消发送 251 | this.voiceToText = '' 252 | } else { 253 | // 发送录音 254 | this.postVoice(res) 255 | } 256 | } 257 | } 258 | 259 | async aboutToAppear() { 260 | this.name = (router.getParams() as ChatPageParam).name 261 | await window.getLastWindow(getContext(this)).then(currentWindow => { 262 | currentWindow.on('keyboardHeightChange', (height) => { 263 | this.keyboardHeight = height; 264 | }) 265 | }) 266 | } 267 | 268 | onBackPress() { 269 | return this.keyboardHeight > 0 270 | } 271 | 272 | // 开始录音 273 | onStartRecord = () => { 274 | // 文件名 唯一 275 | this.recordFileName = Date.now().toString() 276 | AudioCaptureManager.startRecord(this.recordFileName) 277 | } 278 | // 结束录音 279 | stopRecord = async () => { 280 | // res 记录录音文件的路径、时长等信息 这里返回是为了实现 发送录音消息 281 | const res = await AudioCaptureManager.stopRecord() 282 | return res 283 | } 284 | // 生成声音消息 285 | postVoice = (res: RecordFile) => { 286 | // 录音时长 录音结束时间-开始录音时间 287 | const duration = Math.ceil((res.endRecordTime - res.startRecordTime) / 1000) 288 | // 生成消息文件 289 | const voiceChat = new ChatMessage(MessageType.voice, res.recordFilePath, duration) 290 | // 插入到消息数组中 291 | this.chatList.push(voiceChat) 292 | this.dataList.push({ 293 | name: "鲁班", 294 | headSrc: $r("app.media.ic_user_head2"), 295 | msgType: 2, 296 | voicePath: res.recordFilePath, 297 | self: true, 298 | msg: `${duration}"`, 299 | img: $r("app.media.ic_voice_msg_right") 300 | }) 301 | this.scroller.scrollToIndex(this.dataList.length) 302 | } 303 | // 开启ai实时转换声音 304 | onStartSpeechRecognize = () => { 305 | // 如果你是完整的一句话,我把它拼接到 this.voiceToText 如果不是,实时显示 306 | // 缓存一段句子的变量 307 | let sentence = "" 308 | SpeechRecognizerManager.init((res) => { 309 | // console.log("res", JSON.stringify(res)) 310 | // isFinal 这一句话 你结束了没有 311 | // isLast 这一段语音你结束了没有 312 | // this.voiceToText = res.result 313 | if (res.isFinal) { 314 | sentence += res.result 315 | this.voiceToText = sentence 316 | } else { 317 | this.voiceToText = sentence + res.result 318 | } 319 | 320 | }) 321 | } 322 | // 生成文字消息 323 | postText = () => { 324 | // 生成消息文件 325 | const TextChat = new ChatMessage(MessageType.text, this.voiceToText) 326 | // 插入到消息数组中 327 | this.chatList.push(TextChat) 328 | } 329 | 330 | build() { 331 | Column() { 332 | Toolbar({ title: this.name }) 333 | List({ space: 20, initialIndex: this.dataList.length, scroller: this.scroller }) { 334 | ForEach(this.dataList, (item: ChatContentItemData) => { 335 | ListItem() { 336 | if (item.self) { 337 | ListChatContentRightItem({ data: item }) 338 | } else { 339 | ListChatContentLeftItem({ data: item }) 340 | } 341 | } 342 | }) 343 | ListItem() { 344 | Row().height(px2vp(20)) 345 | } 346 | } 347 | .width("100%") 348 | .height(0) 349 | // .divider({ strokeWidth: 0.8, color: '#f0f0f0', startMargin: 90, endMargin: 0 }) // 每行之间的分界线 350 | .layoutWeight(1) 351 | 352 | Row() { 353 | 354 | if (this.inputType == WXInputType.keyboard) { 355 | Image($r("app.media.ic_use_voice")) 356 | .width(33) 357 | .height(33) 358 | .borderRadius(4) 359 | .margin({ left: 15, right: 15 }) 360 | .objectFit(ImageFit.Cover) 361 | .onClick(async () => { 362 | const result = await PermissionUtils.request(["ohos.permission.MICROPHONE"]) 363 | if (!result) { 364 | Toast.show("请先授权麦克风权限") 365 | } 366 | this.inputType = WXInputType.voice 367 | this.showFuncBarWithAnim(false) 368 | }) 369 | 370 | TextArea({ text: $$this.chatText, placeholder: '', controller: this.controller }) 371 | .placeholderFont({ size: 20, weight: 400 }) 372 | .caretColor(Color.Green) 373 | .placeholderColor(Color.Grey) 374 | .layoutWeight(1) 375 | .padding({ left: 8, right: 8 }) 376 | .fontSize(20) 377 | .fontColor(Color.Black) 378 | .borderRadius(8)// 设置stateStyles,解决点击编辑框会变色的问题 379 | .stateStyles({ 380 | normal: {.backgroundColor(Color.White) 381 | }, 382 | pressed: {.backgroundColor(Color.White) 383 | }, 384 | clicked: {.backgroundColor(Color.White) 385 | }, 386 | focused: {.backgroundColor(Color.White) 387 | } 388 | }) 389 | // .onSubmit(() => { 390 | // prompt.showToast({ message: this.chatText }) 391 | // }) 392 | } else { 393 | Image($r("app.media.ic_use_keyboard")) 394 | .width(33) 395 | .height(33) 396 | .borderRadius(4) 397 | .margin({ left: 15, right: 15 }) 398 | .objectFit(ImageFit.Cover) 399 | .onClick(() => { 400 | this.inputType = WXInputType.keyboard 401 | }) 402 | 403 | Button("按住 说话") 404 | .fontColor(Color.Black) 405 | .layoutWeight(1) 406 | .type(ButtonType.Normal) 407 | .borderRadius(8) 408 | .backgroundColor(Color.White) 409 | .fontSize(18) 410 | .bindContentCover($$this.showTalkContainer, this.buildRecordVoiceView, 411 | { modalTransition: ModalTransition.NONE }) 412 | .onTouch(this.onPressTalk) 413 | } 414 | 415 | 416 | Image($r("app.media.ic_emoji_pack")) 417 | .width(33) 418 | .height(33) 419 | .borderRadius(4) 420 | .margin({ left: 15, right: 5 }) 421 | .objectFit(ImageFit.Cover) 422 | .onClick(unDevTip) 423 | 424 | 425 | Text("发送") 426 | .fontColor(Color.White) 427 | .width(65) 428 | .textAlign(TextAlign.Center) 429 | .height(36) 430 | .backgroundColor("#58BE6A") 431 | .fontSize(18) 432 | .visibility(this.chatText.length == 0 ? Visibility.None : Visibility.Visible) 433 | .borderRadius(6) 434 | .margin({ left: 10, right: 10 }) 435 | .onClick(() => { 436 | this.sendTextMsg() 437 | }) 438 | 439 | Image($r("app.media.ic_add")) 440 | .width(33) 441 | .height(33) 442 | .borderRadius(4) 443 | .visibility(this.chatText.length == 0 ? Visibility.Visible : Visibility.None) 444 | .margin({ left: 15, right: 8 }) 445 | .objectFit(ImageFit.Cover) 446 | .onClick(() => { 447 | if (!this.showFuncBar) { 448 | this.inputType = WXInputType.keyboard 449 | } 450 | this.showFuncBarWithAnim(!this.showFuncBar) 451 | }) 452 | 453 | } 454 | .padding({ top: 10, bottom: 10 }) 455 | .alignItems(VerticalAlign.Bottom) 456 | .backgroundColor("#f9f9f9") 457 | 458 | Divider() 459 | .vertical(false) 460 | .color("#f0f0f0") 461 | .strokeWidth(1) 462 | .lineCap(LineCapStyle.Round) 463 | 464 | ChatFunctionBar() 465 | .height(this.funcBarHeight) 466 | 467 | } 468 | .width("100%") 469 | .height("100%") 470 | .backgroundColor("#f1f1f1") 471 | .padding({ bottom: WindowManager.navBarHeight }) 472 | } 473 | 474 | private sendTextMsg() { 475 | this.dataList.push({ 476 | name: "鲁班", 477 | headSrc: $r("app.media.ic_user_head2"), 478 | msgType: 0, 479 | self: true, 480 | msg: this.chatText, 481 | }) 482 | this.scroller.scrollToIndex(this.dataList.length) 483 | this.chatText = "" 484 | } 485 | 486 | private showFuncBarWithAnim(show: boolean) { 487 | animateTo({ duration: 400, curve: Curve.Ease }, () => { 488 | if (show) { 489 | this.funcBarHeight = 250; 490 | } else { 491 | this.funcBarHeight = 0; 492 | } 493 | this.showFuncBar = show; 494 | }) 495 | } 496 | 497 | @Builder 498 | buildRecordVoiceView() { 499 | Column() { 500 | // 中心波浪线 501 | Row() { 502 | if (this.pressCancelVoicePostText !== PressCancelVoicePostText.postText) { 503 | // 声纹 504 | this.vocalPrint() 505 | 506 | } else { 507 | Scroll() { 508 | // 显示录音的文字 509 | Text(this.voiceToText) 510 | .fontSize(12) 511 | .fontColor("#666") 512 | } 513 | .width("100%") 514 | .height("100%") 515 | } 516 | 517 | Text() 518 | .width(10) 519 | .height(10) 520 | .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.Red : 521 | "#95EC6A") 522 | .position({ 523 | bottom: -5, 524 | left: "50%" 525 | }) 526 | .translate({ 527 | x: "-50%" 528 | }) 529 | .rotate({ 530 | angle: 45 531 | }) 532 | } 533 | .width("50%") 534 | .height(80) 535 | .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.Red : "#95EC6A") 536 | .position({ 537 | top: "40%", 538 | left: "50%" 539 | }) 540 | .translate({ 541 | x: "-50%" 542 | }) 543 | .borderRadius(10) 544 | .justifyContent(FlexAlign.Center) 545 | .alignItems(VerticalAlign.Center) 546 | 547 | // 取消和转文字 548 | Row() { 549 | // X 550 | Row() { 551 | Text("X") 552 | .fontSize(20) 553 | .width(80) 554 | .height(80) 555 | .borderRadius("50%") 556 | .fontColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#333" : Color.Gray) 557 | .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.White : 558 | "#333") 559 | .textAlign(TextAlign.Center) 560 | .align(Alignment.Center) 561 | .fontColor("#ccc") 562 | .id("cancel_record") 563 | .rotate({ angle: -15 }) 564 | } 565 | 566 | // 文 567 | Row() { 568 | Text("文") 569 | .fontSize(20) 570 | .width(80) 571 | .height(80) 572 | .borderRadius("50%") 573 | .fontColor(this.pressCancelVoicePostText === PressCancelVoicePostText.postText ? "#333" : Color.Gray) 574 | .backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.postText ? Color.White : "#333") 575 | .textAlign(TextAlign.Center) 576 | .align(Alignment.Center) 577 | .id("translate_text") 578 | .rotate({ angle: 15 }) 579 | } 580 | 581 | // 3 松开发送 582 | Text(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? '取消发送' : 583 | (this.pressCancelVoicePostText === PressCancelVoicePostText.postText ? '转换文字' : "松开发送")) 584 | .fontColor("#fff") 585 | .width("100%") 586 | .position({ 587 | bottom: 0, 588 | left: 0 589 | }) 590 | .textAlign(TextAlign.Center) 591 | } 592 | .width("100%") 593 | .position({ 594 | bottom: "23%" 595 | }) 596 | .justifyContent(FlexAlign.SpaceBetween) 597 | .padding({ 598 | left: 35, right: 35 599 | }) 600 | 601 | 602 | Stack() { 603 | Text() 604 | .width(800) 605 | .height(800) 606 | .backgroundColor("#AAAAAA") 607 | .borderRadius("50%")// .position({ top: 0 ,left:"-50%"}) 608 | // .position({ top: 0 ,left:"-50%"}) 609 | .position({ top: 0, left: "50%" }) 610 | .translate({ 611 | x: "-50%", 612 | }) 613 | 614 | Image($r("app.media.ic_voice_msg_left")) 615 | .width(33) 616 | .height(33) 617 | .objectFit(ImageFit.Cover) 618 | } 619 | .width("100%") 620 | .height(150) 621 | .alignContent(Alignment.Center) 622 | .position({ 623 | bottom: 0 624 | }) 625 | .id("bottom_bubble") 626 | .onAppear(() => { 627 | let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById("bottom_bubble"); 628 | this.bubbleScreenOffset.x = px2vp(modePosition.localOffset.x) 629 | this.bubbleScreenOffset.y = px2vp(modePosition.localOffset.y) 630 | this.bubbleScreenOffset.width = px2vp(modePosition.size.width) 631 | this.bubbleScreenOffset.height = px2vp(modePosition.size.height) 632 | }) 633 | 634 | } 635 | .width("100%") 636 | .height("100%") 637 | .backgroundColor("#9F000000") 638 | } 639 | 640 | /** 641 | * 录音中的 动态声纹波浪 642 | */ 643 | @Builder 644 | vocalPrint() { 645 | Canvas(this.context) 646 | .onDisAppear(() => { 647 | clearInterval(this.voiceTimeId) 648 | }) 649 | .width('80%') 650 | .height('80%') 651 | .onReady(() => { 652 | clearInterval(this.voiceTimeId) 653 | this.voiceTimeId = setInterval(() => { 654 | this.context.clearRect(0, 0, 200, 200) 655 | for (let index = 0; index < 35; index++) { 656 | const random = Math.floor(Math.random() * 20) 657 | let height = 5 + random 658 | this.context.fillRect(10 + index * 5, 32 - height / 2, 2, height); 659 | } 660 | }, 100) 661 | }) 662 | } 663 | 664 | private async startVibration() { 665 | try { 666 | // 触发马达振动 667 | vibrator.startVibration({ 668 | type: 'time', 669 | duration: 100, 670 | }, { 671 | id: 0, 672 | usage: 'touch' 673 | }, () => { 674 | }) 675 | } catch (err) { 676 | } 677 | } 678 | } 679 | 680 | 681 | const unDevTip = () => { 682 | Toast.show("功能正在开发中...") 683 | } 684 | 685 | --------------------------------------------------------------------------------