更新云信和公益咨询相关代码

This commit is contained in:
XiuYun CHEN 2025-07-11 17:33:40 +08:00
parent 2da6409a0b
commit 879646296d
26 changed files with 864 additions and 71 deletions

View File

@ -5,6 +5,7 @@
*
*/
import { authStore, BasicConstant, patientDbManager } from '@itcast/basic';
import { ChatKitClient } from '@nimkit/chatkit';
import { V2NIMMessagePin } from '@nimsdk/base';
import { V2NIMMessage, V2NIMMessageSendingState } from '@nimsdk/base/src/main/ets/nim/sdk/V2NIMMessageService';
@ -67,7 +68,12 @@ export class ChatInfo {
};
getCurrentUserAvatarUrl(message?: V2NIMMessage): string {
return '';
return BasicConstant.urlHtml+authStore.getUser().photo;
};
getCurrentUserAvatarName(): string {

View File

@ -23,6 +23,7 @@ import {
NECommonUtils,
NetworkBrokenBuilder,
PermissionsUtils,
UserUtils,
VideoViewerDialog
} from '@nimkit/common';
import { NECameraSelectView } from '../view/NECameraSelectView';
@ -179,6 +180,7 @@ export struct ChatP2PPage {
cornerRadius: 4,
width: '70%',
})
@Local showName:string=String(this.chatUserInfo.conversationName)
waDialog: CustomDialogController = new CustomDialogController({
builder: NewWaDialog({
firstCallBack:()=>{
@ -401,7 +403,7 @@ export struct ChatP2PPage {
})
}
patientDetail()
patientDetail()
{
this.sessionId = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(this.conversationId)
this.patientUuid=this.sessionId
@ -411,14 +413,13 @@ export struct ChatP2PPage {
let json = JSON.parse(res+'') as Record<string, object>;
let datas=json.data as Record<string, string>;
this.patientUuid=datas.uuid
this.showName=await UserUtils.getAvatarName(this.patientUuid,String(this.chatUserInfo.conversationName))
}).catch((err: BusinessError) => {
})
}
aboutToAppear(): void {
aboutToAppear() {
NEEmojiManager.instance.setup();
DeviceUtils.rootDirPath = getContext(this).filesDir
this.operationMoreDataList = setupMoreOperationData()
@ -455,7 +456,7 @@ export struct ChatP2PPage {
})
this.initDialog()
this. getOutList()
this.patientDetail()
}
computeScrollHeight() {
@ -522,6 +523,7 @@ export struct ChatP2PPage {
this.chatUserInfo.setConversationId(this.conversationId)
this.chatViewModel.init(this.conversationId, this.chatUserInfo);
this.chatViewModel.loadData();
this.patientDetail()
}
async showImageDetail(msg?: NIMMessageInfo, onlyMsg?: boolean) {
@ -571,7 +573,7 @@ export struct ChatP2PPage {
build() {
NavDestination() {
NavigationBackBuilder({
title: this.chatUserInfo.conversationName,
title:this.showName ,
rightButtonIcon: this.showMultiSelect ? undefined : $r('app.media.ic_public_more_dot'),
rightButtonTitle: this.showMultiSelect ? $r('app.string.chat_msg_dialog_cancel') : undefined,
top:this.statusBarHeight,

View File

@ -13,6 +13,7 @@ import { AvatarColorUntil, AvatarItem, CommonAvatar } from '@nimkit/common';
import { messageContent } from './MessageComponentBuilder';
import { MessageItemClick } from './MessageItemClick';
import { ChatKitConfig } from '../ChatKitConfig';
import { authStore, BasicConstant } from '@itcast/basic';
/**
* 消息组件按照UI样式划分为发送消息组件、接收消息组件、通知消息组件和提示消息组件
@ -243,7 +244,7 @@ export struct SenderMessageComponent {
CommonAvatar({
item: new AvatarItem(this.chatUserInfo != null ?
this.chatUserInfo?.getCurrentUserAvatarUrl() : '',
this.chatUserInfo?.getCurrentUserAvatarUrl() : BasicConstant.urlHtml+authStore.getUser().photo,
this.chatUserInfo?.getCurrentUserAvatarName() ?? '',
AvatarColorUntil.getBackgroundColorById(this.message.message.senderId),
),

View File

@ -60,6 +60,7 @@ import { instanceToPlain } from 'class-transformer';
import { NECommonUtils } from '@nimkit/common';
import { HashMap, JSON } from '@kit.ArkTS';
import { assign } from '@nimsdk/vendor';
import { BasicConstant, ChatExtModel } from '@itcast/basic';
@ObservedV2
export class ChatBaseViewModel {
@ -681,13 +682,20 @@ export class ChatBaseViewModel {
async sendTextMessage(text: string, replyMsg?: NIMMessageInfo, aitModel?: AitModel, pushList?: string[]) {
const message = ChatRepo.createTextMessage(text)
//设置Ait
if (aitModel && aitModel.aitBlocks.size > 0) {
let extensionMap: YxAitMsg = {
yxAitMsg: aitModel.aitBlocks
}
let extension = JSON.stringify(instanceToPlain(extensionMap))
message.serverExtension = extension
// if (aitModel && aitModel.aitBlocks.size > 0) {
// let extensionMap: YxAitMsg = {
// yxAitMsg: aitModel.aitBlocks
// }
// let extension = JSON.stringify(instanceToPlain(extensionMap))
// message.serverExtension = extension
// }
let extension:ChatExtModel= {
gdxz_sessionType: '',
gdxz_consult_uuid: '',
gdxz_nickName: ''
}
message.serverExtension =JSON.stringify(extension)
//设置推送
let params: V2NIMSendMessageParams | undefined = undefined
@ -742,6 +750,7 @@ export class ChatBaseViewModel {
}
params.aiConfig = aiParams
}
this.sendMessage(message, params)
}

View File

@ -75,4 +75,6 @@ export { MatchSearchText } from './src/main/ets/view/MatchSearchText'
export {CommonLongLoadingProgress } from './src/main/ets/builder/CommonLongLoadingProgress'
export { BreakpointConstants} from './src/main/ets/constants/BreakpointConstants'
export { BreakpointConstants} from './src/main/ets/constants/BreakpointConstants'
export { UserUtils } from './src/main/ets/utils/UserUtils';

View File

@ -6,6 +6,7 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@ohos/pinyin4js": "^2.0.0"
"@ohos/pinyin4js": "^2.0.0",
"@itcast/basic":"file:../commons/basic"
}
}

View File

@ -0,0 +1,22 @@
import { patientDbManager } from "@itcast/basic"
export class UserUtils {
static async getAvatarName(account: string,names:string){
let patient =await patientDbManager.getPatientByUuid(account)
if(patient!=null)
{
if(patient.nickname!=null&&patient.nickname!='')
{
return patient.nickname
}
if(patient.realName!=null&&patient.realName!='')
{
return patient.realName
}
}
return String(names)
}
}

View File

@ -4,7 +4,7 @@
* found in the LICENSE file.
*
*/
import { authStore, BasicConstant, patientDbManager } from '@itcast/basic';
@ComponentV2
export struct CommonAvatar {
/**
@ -29,24 +29,48 @@ export struct CommonAvatar {
if (this.item != null) {
if (this.item.avatarUrl == null || this.item.avatarUrl.toString().length <= 0) {
Stack() {
Column() {
}.width('100%').height('100%').backgroundColor(this.item.color)
.borderRadius(this.roundRadius)
Image(BasicConstant.urlHtml+authStore.getUser().photo)
.onError(() => {
if(this.item!=null)
{
this.item.avatarUrl = $r('app.media.icon_touxiang_persion_new');
}
})
.syncLoad(true)
.borderRadius(this.roundRadius)
.onGestureJudgeBegin(() => {
return GestureJudgeResult.CONTINUE;
}).parallelGesture(LongPressGesture().onAction(event => {
this.longPressGesture?.(event)
}
))
// Stack() {
// Column() {
// }.width('100%').height('100%').backgroundColor(this.item.color)
// .borderRadius(this.roundRadius)
//
// Text(this.item.name)
// .fontSize(this.textSize)
// .fontColor(this.textColor)
// .ellipsisMode(EllipsisMode.END)
// .textAlign(TextAlign.Center)
// .margin({
// left: 6, right: 6
// })
// .width('100%')
// .maxLines(1)
// }.gesture(LongPressGesture().onAction(this.longPressGesture))
Text(this.item.name)
.fontSize(this.textSize)
.fontColor(this.textColor)
.ellipsisMode(EllipsisMode.END)
.textAlign(TextAlign.Center)
.margin({
left: 6, right: 6
})
.width('100%')
.maxLines(1)
}.gesture(LongPressGesture().onAction(this.longPressGesture))
} else {
Image(this.item.avatarUrl)
.onError(() => {
if(this.item!=null)
{
this.item.avatarUrl = $r('app.media.icon_touxiang_persion_new');
}
})
.syncLoad(true)
.borderRadius(this.roundRadius)
.onGestureJudgeBegin(() => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -85,4 +85,6 @@ export { ViewImageInfo } from './src/main/ets/models/ViewImageInfo'
export { ChangePhotoGrids } from './src/main/ets/components/ChangePhotoGrids'
export { InputPopWindow } from './src/main/ets/Views/InputPopWindow'
export { InputPopWindow } from './src/main/ets/Views/InputPopWindow'
export { ChatExtModel } from './src/main/ets/models/ChatExtModel'

View File

@ -14,7 +14,7 @@ export struct PerfactInputSheet {
@State okText:ResourceStr='确定'
@State cancelText:ResourceStr='取消'
private inputCallBack: (input: string,title:string) => void = () => {};
@State needcancelCallBack:boolean=false
// 修改构造函数
constructor(controller: CustomDialogController, inputCallBack: (input: string,title:string) => void) {
super();
@ -115,6 +115,8 @@ export struct PerfactInputSheet {
.textAlign(TextAlign.Center)
.width('45%').height(30)
.onClick(() => {
this.controller.close()
})
Text('').height(30).width(1)
@ -171,6 +173,10 @@ export struct PerfactInputSheet {
.width('45%').height(30)
.onClick(() => {
if(this.needcancelCallBack)
{
this.inputCallBack(this.inputText, 'needcancelCallBack');
}
this.controller.close()
})
Text('').height(30).width(1)

View File

@ -79,6 +79,7 @@ export class BasicConstant {
static readonly listMyAnsweredInterrogation = BasicConstant.urlExpertAPI +"listMyAnsweredInterrogation";// 一问多答 我回答的一问多答列表
static readonly getInterrogation = BasicConstant.urlExpertAPI +"getInterrogation";// 一问多答 详情页
static readonly InterrogationPatientInfo = BasicConstant.urlExpertAPI +"InterrogationPatientInfo";// 一问多答 患者详情页
static readonly updateInterrogationAnswer = BasicConstant.urlExpertAPI+"updateInterrogationAnswer";// 一问多答 编辑回答
static readonly province=['全国','北京市','天津市','河北省','山西省'
,'内蒙古自治区','辽宁省','吉林省','黑龙江省','上海市','江苏省','浙江省'
,'安徽省','福建省','江西省','山东省','河南省','湖北省','湖南省','广东省',

View File

@ -0,0 +1,8 @@
export interface ChatExtModel{
gdxz_sessionType:string,//是否为公益咨询
gdxz_consult_uuid:string,//公益咨询uuid
gdxz_nickName:string//患者备注姓名
}

View File

@ -8,6 +8,7 @@ import { fileIo } from '@kit.CoreFileKit';
import util from '@ohos.util';
import { i18n } from '@kit.LocalizationKit';
import { connection } from '@kit.NetworkKit';
import http from '@ohos.net.http'
export class ChangeUtil {
/**
* 将HashMap转成JsonString
@ -191,10 +192,46 @@ export class ChangeUtil {
}
static stringIsUndefinedAndNull (string:string | undefined):boolean {
if (string == undefined || string == "null" || string == "<null>" || string == "(null)" || string == 'undefined' || string.length <= 0) {
if (string == null||string == undefined || string == "null" || string == "<null>" || string == "(null)" || string == 'undefined' || string.length <= 0) {
return true
} else {
return false
}
}
static map2JsonO(map:HashMap<string, Object>) {
let jsonObject: Record<string, Object> = {};
map.forEach((value, key) => {
if(key != undefined && value != undefined){
jsonObject[key] = value;
}
})
return jsonObject;
}
static async getImageBase64(url: string): Promise<string> {
// 创建 http 实例
let httpRequest = http.createHttp();
// 发起 GET 请求
let response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
// 重要:设置 responseType 为 arraybuffer获取二进制数据
expectDataType: http.HttpDataType.ARRAY_BUFFER
});
// 检查响应
if (response.responseCode === 200 && response.result) {
// 1. 转成 Uint8Array
const buffernew=await ChangeUtil.compression(response.result as ArrayBuffer,"image/jpeg",0.5)
// let uint8Arr = new Uint8Array(response.result as ArrayBuffer)
const base64Helper = new util.Base64Helper();
return base64Helper.encodeToStringSync(new Uint8Array(buffernew));
// response.result 是 ArrayBuffer
// let base64Str = base64.encode(response.result as ArrayBuffer);
// // 可选:拼接 data url 前缀
// return "data:image/png;base64," + base64Str;
}
return "";
}
}

View File

@ -249,8 +249,7 @@ export class PatientDao {
if (!this.rdbStore) {
throw new Error('数据库连接为空');
}
const sql = 'SELECT * FROM patients WHERE uuid = ?';
const sql = 'SELECT * FROM patients WHERE LOWER(uuid) = LOWER(?)';
const resultSet = await this.rdbStore.querySql(sql, [uuid]);
if (resultSet.rowCount > 0) {

View File

@ -67,7 +67,40 @@ class HdHttp {
httpInstance.destroy()
})
}
private requestObject<T>(path: string, method: http.RequestMethod = http.RequestMethod.POST, extraDatas :HashMap<string, Object>) {
const httpInstance = http.createHttp()
const options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
// 可选默认为60s
connectTimeout: 60000,
// 可选默认为60s
readTimeout: 60000,
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json',
'sign':this.getSignO(extraDatas),
'User-Agent':this.osFullName
},
extraData:ChangeUtil.map2JsonO(extraDatas)
}
let fullUrl = this.baseURL + path
return httpInstance.request(fullUrl, options).then((res) => {
logger.info('Response param'+JSON.stringify(extraDatas))
logger.info('Response fullUrl:' +fullUrl+ res.result);
const result = res.result as HdResponse<T>
return result
}).catch((err: BusinessError) => {
logger.info(fullUrl+`Response succeeded: ${err}`);
promptAction.showToast({ message: err.message || '网络错误' })
return Promise.reject(err)
}).finally(() => {
httpInstance.destroy()
})
}
private requestafter<T>(path: string, method: http.RequestMethod = http.RequestMethod.GET, extraData?: Object) {
const httpInstance = http.createHttp()
@ -129,6 +162,9 @@ class HdHttp {
posts<T>(url: string, data: HashMap<string, string>): Promise<HdResponse<T>> {
return this.request<T>(url, http.RequestMethod.POST, data)
}
postO<T>(url: string, data: HashMap<string, Object>): Promise<HdResponse<T>> {
return this.requestObject<T>(url, http.RequestMethod.POST, data)
}
httpReq<T>(url: string, datas: HashMap<string, string>): Promise<HdResponse<T>> {
// 创建httpRequest对象。
let httpRequest = http.createHttp();
@ -171,6 +207,47 @@ class HdHttp {
})
}
httpReqObject<T>(url: string, datas: HashMap<string, Object>): Promise<HdResponse<T>> {
// 创建httpRequest对象。
let httpRequest = http.createHttp();
let url1 = "https://dev-app.igandan.com/app/manager/getSystemTimeStamp";
let promise = httpRequest.request(
// 请求url地址
url1,
{
// 请求方式
method: http.RequestMethod.GET,
// 可选默认为60s
connectTimeout: 60000,
// 可选默认为60s
readTimeout: 60000,
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
}
});
// 处理响应结果。
return promise.then(async (data) => {
if (data.responseCode === http.ResponseCode.OK) {
logger.info('Response httpReq:' + data.result);
let json:TimestampBean = JSON.parse(data.result.toString()) as TimestampBean;
let tp = json.timestamp;
datas.set("user_uuid", authStore.getUser().uuid?authStore.getUser().uuid:'');
datas.set("client_type", 'H');
datas.set("version", await this.getVersion() );
datas.set('timestamp',tp+'');
return this.postO<T>(url, datas);
} else {
return this.postO<T>(url, datas);
}
}
).catch((err:BusinessError) => {
logger.info('Response httpReq error:' + JSON.stringify(err));
return Promise.reject(err);
}).finally(() => {
httpRequest.destroy()
})
}
httpReqSimply<T>(url: string) {
// 创建httpRequest对象。
let httpRequest = http.createHttp();
@ -203,6 +280,29 @@ class HdHttp {
})
}
getSignO(extraDatas1:HashMap<string, Object>): string {
let secret= extraDatas1.get("timestamp")+''
if(secret!=null) {
let keyValueStr: string = "";
let entriesArray: Array<string> = Array.from(extraDatas1.keys());
entriesArray.sort();
let sortedMap:HashMap<string, Object> = new HashMap();
entriesArray.forEach((value: string, index: number) => {
sortedMap.set(value,extraDatas1.get(value));
// keyValueStr +=value+extraDatas1.get(value)
keyValueStr +=value+JSON.stringify(extraDatas1.get(value))
});
keyValueStr = keyValueStr + CryptoJS.MD5(secret).toString();
keyValueStr = keyValueStr.replaceAll(" ", "").replaceAll("\"", "").replaceAll(":","=");
let Md5keyValueStr: string = CryptoJS.MD5(keyValueStr).toString();
let base64Str:string=Base64Util.encodeToStrSync(Md5keyValueStr);
return base64Str;
} else {
return '';
}
}
getSign(extraDatas1:HashMap<string, string>): string {
let secret= extraDatas1.get("timestamp")
if(secret!=null) {

View File

@ -1,6 +1,6 @@
import { iconsModel } from '../model/HomeModel'
import { patientDbManager, PatientEntity } from '@itcast/basic';
import { promptAction } from '@kit.ArkUI';
import { promptAction, router } from '@kit.ArkUI';
// interface iconsModel {
// img:string;
@ -57,6 +57,13 @@ export struct HomeIconComp {
const patients = await patientDbManager.getAllPatients();
console.info(`添加了 ${patients.length} 个患者`);
promptAction.showToast({message:`添加了 ${patients.length} 个患者`})
if(item.name=='公益咨询') {
router.pushUrl({ url: 'pages/Netease/PublicConsultationPage' })
}
else if(item.name=='我的患者') {
router.pushUrl({url:'pages/Netease/imTabPage'})
}
})
})
}.width('100%').backgroundColor(Color.White)

View File

@ -198,9 +198,9 @@ export struct VideoPage {
})
.margin({bottom:60})
.onClick(() => {
// router.pushUrl({url:'pages/VideoPage/PastVideoPage'})
router.pushUrl({url:'pages/VideoPage/PastVideoPage'})
// router.pushUrl({url:'pages/Netease/imTabPage'})
router.pushUrl({url:'pages/Netease/PublicConsultationPage'})
// router.pushUrl({url:'pages/Netease/PublicConsultationPage'})
})
}

View File

@ -32,6 +32,8 @@ export struct ConsultationDetailComp {
builder:PerfactInputSheet({
controller:this.custom,
inputTitle:'',
okText:'是',
cancelText:'否',
inputPlaceholder:this.inputPlaceholder,
style:'2',
okColor:$r('app.color.top_title'),

View File

@ -263,11 +263,21 @@ export struct InterrogationDetailComp {
.backgroundColor($r('app.color.patient_theme'))
.fontColor(Color.White)
.onClick(() => {
if(this.params.isHistory =='false')
{
router.pushUrl({
url: 'pages/Netease/MyOpinionPage',
params: { uuid:this.params.uuid,isHistory:this.params.isHistory}
});
}
else
{
router.pushUrl({
url: 'pages/Netease/MyOpinionPage',
params: { uuid:this.params.uuid,isHistory:this.params.isHistory,myAnswer:this.getMyanswer(this.AnswerList)}
});
}
router.pushUrl({
url: 'pages/Netease/MyOpinionPage',
// params: { uuid: this.item.uuid}
});
})
}
@ -307,5 +317,11 @@ export struct InterrogationDetailComp {
}
getMyanswer(AnswerList:AnswerListBean[])
{
let targetItem = AnswerList.find(item => authStore.getUser().uuid ==item.expert_uuid );
return targetItem
}
}

View File

@ -1,6 +1,16 @@
import { ChangePhotoGrids, HdNav, ViewImageInfo } from "@itcast/basic"
import { BasicConstant, ChangePhotoGrids,
ChangeUtil,
hdHttp,
HdLoadingDialog, HdNav,
HdResponse,
preferenceStore, ViewImageInfo } from "@itcast/basic"
import { PhotoActionSheet } from '@itcast/basic'
import { AnswerListBean } from "../model/ConsulModel"
import { router } from "@kit.ArkUI"
import { PerfactInputSheet } from "@itcast/basic/src/main/ets/Views/PerfactInputSheet"
import { BusinessError } from "@kit.BasicServicesKit"
import { HashMap } from "@kit.ArkTS"
import { rcp } from '@kit.RemoteCommunicationKit';
@Component
export struct MyOpinionComp {
@State photos: string[] = []
@ -10,12 +20,15 @@ export struct MyOpinionComp {
@State
@Watch('onRemoveImg')
removeImg: boolean=false
@State text: string = ''
@State
@Watch('onAddImg')
addImg: boolean=false
@State params:param = router.getParams() as param;
@State removeIndex: number=0
hashMap: HashMap<string, Object> = new HashMap();
hashMapImg: HashMap<string, string> = new HashMap();
onAddImg()
{
this.photoSheetDialog.open()
@ -25,6 +38,42 @@ export struct MyOpinionComp {
this.photos.splice(this.removeIndex, 1)
this.maxSelectNumber = this.maxSelectNumber - this.photos.length;
}
private custom!:CustomDialogController;
dialog: CustomDialogController = new CustomDialogController({
builder: HdLoadingDialog({ message: '加载中...' }),
customStyle: true,
alignment: DialogAlignment.Center
})
initDialog() {
this.custom = new CustomDialogController({
builder:PerfactInputSheet({
controller:this.custom,
inputTitle:'提示',
okText:'保存',
needcancelCallBack:true,
okColor:$r('app.color.top_title'),
inputPlaceholder:'您有未发布的内容,是否保存?',
style:'2',
inputCallBack:(input: string,title:string)=>{
if(title=='needcancelCallBack')
{
preferenceStore.setItemString('MyOpinionComp'+this.params.uuid,'')
}
else
{
preferenceStore.setItemString('MyOpinionComp'+this.params.uuid,this.text)
}
router.back()
}
}),
alignment: DialogAlignment.Center,
customStyle: true,
autoCancel: false,
backgroundColor: ('rgba(0,0,0,0.5)'),
height: '100%'
})
}
private initPhotoDialog() {
this.photoSheetDialog = new CustomDialogController({
builder: PhotoActionSheet({
@ -44,13 +93,7 @@ export struct MyOpinionComp {
}
// onPhotoSelected: async (uri: string) => {
// if (uri && this.photos.length < 9) {
// this.photos.push(uri)
// }
// // this.photoPath = uri;
// // this.base64Stringphoto = await ChangeUtil.convertUriToBase64(uri);
// }
}),
alignment: DialogAlignment.Bottom,
customStyle: true,
@ -60,17 +103,90 @@ export struct MyOpinionComp {
});
}
aboutToAppear(): void {
console.log('Response aboutToAppear')
this.initPhotoDialog()
this.initDialog()
if(this.params.isHistory =='true')
{
this.text=this.params.myAnswer.note
if(this.params.myAnswer.imgs!=null)
{
this.photos.push(...this.changeToImgs(this.params.myAnswer.imgs.split(",")))
this.maxSelectNumber = this.maxSelectNumber - this.photos.length;
}
}
else
{
this.text=preferenceStore.getItemString('MyOpinionComp'+this.params.uuid)
}
}
build() {
Column() {
HdNav({ title: '我的意见', showRightIcon: false, showLeftIcon: true })
HdNav({ title: '我的意见', showRightIcon: false, showLeftIcon: true ,isLeftAction:true,leftItemAction:()=>{
if(this.params.isHistory =='false')
{
if(this.text.length>0)
{
this.custom.open()
}
else
{
router.back()
}
}
else
{
router.back()
}
}})
Text('我的意见 *').fontSize(17).fontColor($r('app.color.top_title')).padding(10).width('100%').textAlign(TextAlign.Start)
TextArea({ placeholder: '请依据患者的个人信息、疾病资料及患者所咨询的问题详细解答患者的问题信息仅提问患者及医生可见、最多输入300个字', text: $$this.text })
.fontColor($r('app.color.common_gray_03'))
.height(100)
.textAlign(TextAlign.Start)
.fontSize(14).padding(9).margin({left:10,right:10,bottom:10})
.backgroundColor($r('app.color.f6f6f6')).borderRadius(8)
Text('相关图片').fontSize(17).fontColor($r('app.color.top_title')).padding({left:10,right:10,bottom:10}).width('100%').textAlign(TextAlign.Start)
Text('可以用部分科普或文献来协助问答问题最多6张').fontSize(14).fontColor($r('app.color.999999')).padding({left:10,right:10,bottom:10}).width('100%').textAlign(TextAlign.Start)
ChangePhotoGrids({imgList:this.changeToImg(this.photos),maxSelectNumber:6
,addImg:this.addImg,removeImg:this.removeImg,removeIndex:this.removeIndex})
.backgroundColor(Color.Red)
,addImg:this.addImg,removeImg:this.removeImg,removeIndex:this.removeIndex}).layoutWeight(1)
Column()
{
Button({ type: ButtonType.Normal }){
Text('提交')
}
.width('100%')
.height(53)
.backgroundColor($r('app.color.patient_theme'))
.fontColor(Color.White)
.onClick(() => {
if(this.params.isHistory =='false')
{
// router.pushUrl({
// url: 'pages/Netease/MyOpinionPage',
// params: { uuid:this.params.uuid,isHistory:this.params.isHistory}
// });
}
else
{
this.updateInterrogationAnswer()
// router.pushUrl({
// url: 'pages/Netease/MyOpinionPage',
// params: { uuid:this.params.uuid,isHistory:this.params.isHistory,myAnswer:this.getMyanswer(this.AnswerList)}
// });
}
})
}
.backgroundColor(Color.White)
.width('100%')
}
@ -78,6 +194,48 @@ export struct MyOpinionComp {
.width('100%')
}
async getUploadImg()
{
this.hashMapImg.clear();
if(this.photos.length>0)
{
for (let index = 0; index < this.photos.length; index++) {
if(this.photos[index].includes('http'))
{
this.hashMapImg.set('img'+index+1,await ChangeUtil.getImageBase64(this.photos[index]))
}
else
{
this.hashMapImg.set('img'+index+1, await ChangeUtil.convertUriToBase64(this.photos[index]))
}
}
}
return this.hashMapImg
}
async updateInterrogationAnswer()
{
this.dialog.open()
this.hashMap.clear();
this.hashMap.set("note",this.text);
this.hashMap.set('uuid', this.params.uuid)
this.hashMap.set('imgsBean',await this.getUploadImg())
hdHttp.httpReqObject<string>(BasicConstant.updateInterrogationAnswer,this.hashMap).then(async (res: HdResponse<string>) => {
this.dialog.close()
}).catch((err: BusinessError) => {
this.dialog.close()
})
// let req = new rcp.Request("http://example.com", "POST", headers, simpleForm, cookies, transferRange, configuration);
}
changeToImg( imgListurl:string[])
{
let imgListtmps:ViewImageInfo[]=[]
@ -88,4 +246,21 @@ export struct MyOpinionComp {
return imgListtmps
}
}
changeToImgs( imgListurl:string[])
{
let imgListtmps:string[]=[]
imgListurl.forEach((url: string) => {
let item = BasicConstant.urlHtml + url
imgListtmps.push(item)
})
return imgListtmps
}
}
interface param{
uuid:string,
isHistory:string,
myAnswer:AnswerListBean
}

View File

@ -5,8 +5,9 @@
*
*/
import { patientDbManager, PatientEntity } from '@itcast/basic';
import { ChatKitClient } from '@nimkit/chatkit';
import { AvatarColorUntil, AvatarItem, CommonAvatar } from '@nimkit/common';
import { AvatarColorUntil, AvatarItem, CommonAvatar, UserUtils } from '@nimkit/common';
import { DateUtil } from '@nimkit/common/src/main/ets/utils/DateUtil';
import { V2NIMLocalConversation, V2NIMMessageCallAttachment, V2NIMMessageType } from '@nimsdk/base';
import { LocalConversationOperationDialog } from './LocalConversationOperationDialog';
@ -25,6 +26,12 @@ export struct ConversationViewItem {
width: '60%',
borderColor: '#ffDCDFE5'
})
@Local showName:string=String(this.conversationInfo?.name)
async aboutToAppear(): Promise<void> {
let account = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(this.conversationInfo?.conversationId)
this.showName=await UserUtils.getAvatarName(account,String(this.conversationInfo?.name))
}
build() {
if (this.conversationInfo !== null) {
@ -59,7 +66,7 @@ export struct ConversationViewItem {
Column() {
//item 显示名称
Text(this.conversationInfo.name)
Text(this.showName)
.fontSize(16)
.fontColor("#ff333333")
.textOverflow({ overflow: TextOverflow.Ellipsis })
@ -160,6 +167,25 @@ export struct ConversationViewItem {
}
}
// async getAvatarName(sourceName: string){
// let account = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(this.conversationInfo?.conversationId)
// let patient =await patientDbManager.getPatientByUuid(account)
// if(patient!=null)
// {
// if(patient.nickname!=null&&patient.nickname!='')
// {
// return patient.nickname
// }
// if(patient.realName!=null&&patient.realName!='')
// {
// return patient.realName
// }
// }
//
//
// return String(this.conversationInfo?.name)
//
// }
/**
* 头像只显示后两位
* @param sourceName

View File

@ -7,10 +7,10 @@
"license": "",
"dependencies": {
"@itcast/basic": "file:../../commons/basic",
"mypage":"file:../../features/mypage",
"home": 'file:../../features/Home',
"register": 'file:../../features/register',
"patient": 'file:../../features/patient',
"mypage": "file:../../features/mypage",
"home": "file:../../features/Home",
"register": "file:../../features/register",
"patient": "file:../../features/patient",
"scene_single_video": "file:../../scene_single_video",
"media-player-common": "file:../../polyv",
"@polyvharmony/media-player-sdk": "2.5.0",
@ -28,7 +28,8 @@
"@nimsdk/nim": "10.9.10",
"@nimsdk/base": "10.9.10",
"@nimkit/corekit": "file:../../corekit",
'@nimkit/chatkit': "file:../../chatkit",
'netease': "file:../../features/netease"
"@nimkit/chatkit": "file:../../chatkit",
"netease": "file:../../features/netease",
}
}

View File

@ -52,9 +52,9 @@ export class NimRepository {
console.debug(`Performance Test im start loginSuccess`)
await this.nim.loginService.login(accountId, token);
console.error('----------- 登录成功 -----------')
router.pushUrl({
url: 'pages/Netease/imTabPage'
});
// router.pushUrl({
// url: 'pages/Netease/imTabPage'
// });
console.debug(`Performance Test im loginSuccess`)
ChatKitClient.init(this.nim, appKey)
@ -62,6 +62,7 @@ export class NimRepository {
} catch (error) {
console.error('----------- 登录失败 -----------', error)
ChatKitClient.init(this.nim, appKey)
throw error as Error
}
}

View File

@ -0,0 +1,344 @@
import webview from '@ohos.web.webview';
import { HdNav } from '@itcast/basic';
import router from '@ohos.router';
import { image } from '@kit.ImageKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';
import { componentSnapshot } from '@kit.ArkUI';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { display} from '@kit.ArkUI';
const TAG = 'WebViewSaveImage';
@Entry
@Component
struct WebPageSnapshot {
private controller: webview.WebviewController = new webview.WebviewController();
@State params:RouteParams = router.getParams() as RouteParams;
@State contentWidth: number = 0;
@State contentHeight: number = 0;
@State url: string = this.params.url;
@State title: string = this.params.title;
customUserAgent: string = 'gdxz-expert';
// 网页尺寸
@State h5Width: number = 0;
@State h5Height: number = 0;
// Web组件尺寸
private webWidth: number = 0;
private webHeight: number = 0;
// 当前网页位置
private curXOffset: number = 0;
private curYOffset: number = 0;
// 备份当前网页位置
private xOffsetBefore: number = 0;
private yOffsetBefore: number = 0;
// 截图过程的Web组件覆盖
@State webMaskImage: PixelMap | undefined = undefined;
private webMaskImageZIndex: number = -1;
// 合并后的图片
@State mergedImage: PixelMap | undefined = undefined;
@State snapPopupPosition: Position = { x: 0, y: 0 };
//Web是否已经滚动到底部
@State WebTouchBottom: boolean = false;
// 屏幕尺寸
private displayWidth: number = 0;
private displayHeight: number = 0;
onBackPress(): boolean | void {
if (this.controller.accessStep(-1)) {
this.controller.backward();
return true;
}
return false;
}
build() {
Column() {
HdNav({ title: this.title, showRightIcon: false, hasBorder: true ,isLeftAction:true,leftItemAction:()=>{
if (this.controller.accessBackward()) {
this.controller.backward();
} else {
router.back();
}
}})
Web({
src: this.url,
controller: this.controller
})
.id('webViewShot')
.mixedMode(MixedMode.All)
.overScrollMode(OverScrollMode.ALWAYS)
.domStorageAccess(true)
.onAreaChange((oldValue, newValue) => {
// TODO: 高性能知识点: onAreaChange为高频回调组件变动时每帧都会调用避免冗余和耗时操作。
this.webWidth = newValue.width as number;
this.webHeight = newValue.height as number;
})
.onControllerAttached(() => {
let userAgent = this.controller.getUserAgent() + this.customUserAgent;
this.controller.setCustomUserAgent(userAgent);
})
.onOverScroll((event) => {
if (event?.yOffset > 0) {
this.WebTouchBottom = true
} else if (event?.yOffset === 0 && this.WebTouchBottom) {
this.WebTouchBottom = false
}
})
.onScroll((event) => {
this.curXOffset = event.xOffset;
this.curYOffset = event.yOffset;
})
.width('100%')
.layoutWeight(1)// 占据剩余空间
.height('100%')
if (this.title == '随访二维码') {
Row(){
SaveButton({text:SaveDescription.SAVE_IMAGE,buttonType:ButtonType.Normal})
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('rgb(63,199,193)')
.width('100%').height(50)
.onClick( () => {
this.snapShot();
})
}.width('100%').height(56).backgroundColor(Color.White).alignItems(VerticalAlign.Top)
}
}
.height('100%')// 关键:约束父容器高度
}
aboutToAppear(): void {
let a=this.url
// 获取屏幕尺寸
const displayData = display.getDefaultDisplaySync();
this.displayWidth = px2vp(displayData.width);
this.displayHeight = px2vp(displayData.height);
}
async saveWebViewImage() {
try {
// 1. 获取整个网页的长截图
this.controller.webPageSnapshot({
id: "webView",
size: { width: this.contentWidth, height:this.contentHeight } // 确保捕获全尺寸
}, async (error, result) => {
if (error) {
promptAction.showToast({ message: `截图失败: ${error.message}` });
console.error('webPageSnapshot error:', error);
return;
}
if (!result?.imagePixelMap) {
promptAction.showToast({ message: '获取截图数据失败' });
return;
}
// 2. 转为JPEG并写入沙箱
const ctx = getContext(this);
const imagePacker = image.createImagePacker();
const arrayBuffer = await imagePacker.packToData(
result.imagePixelMap,
{ format: 'image/jpeg', quality: 100 } // 平衡质量与大小
);
const sandboxPath = ctx.cacheDir + `/web_fullpage_${Date.now()}.jpg`;
const file = fileIo.openSync(sandboxPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, arrayBuffer);
fileIo.closeSync(file.fd);
// 3. 保存到相册
const helper = photoAccessHelper.getPhotoAccessHelper(ctx);
const sandboxUri = fileUri.getUriFromPath(sandboxPath);
const request = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(ctx, sandboxUri);
await helper.applyChanges(request);
promptAction.showToast({ message: '网页已保存至相册' });
});
} catch (e) {
promptAction.showToast({ message: '保存失败: ' + e.message });
console.error('saveWebViewImage error:', e);
}
}
/**
* 截图函数。
*/
async snapShot() {
// 获取Web页面尺寸
this.getWebSize();
// 截图前的状态初始化
await this.beforeSnapshot();
// TODO: 性能知识点: 使用Canvas离屏绘制在缓冲区拼接截图
const canvasSetting: RenderingContextSettings = new RenderingContextSettings(true);
const offCanvasCtx: OffscreenCanvasRenderingContext2D =
new OffscreenCanvasRenderingContext2D(this.h5Width, this.h5Height, canvasSetting);
// 前置常量
const snipTimes = Math.ceil(this.h5Height / this.webHeight);
const lastTime = snipTimes - 1;
const leftoverHeight = this.h5Height % this.webHeight;
let cropLeftover: image.Region = { x: 0, y: 0, size: { height: 0, width: 0 } }
if (this.WebTouchBottom) {
// 这里要分两种情况1.滚动到底部时,裁剪应该取最后一张除去重复部分以外的底部
cropLeftover = {
x: 0,
y: vp2px(this.webHeight - leftoverHeight),
size: {
height: vp2px(leftoverHeight),
width: vp2px(this.webWidth)
}
};
} else {
// 2.未滚动到底部时裁剪应该取最后一张leftoverHeight的上部分
cropLeftover = {
x: 0,
y: 0,
size: {
height: vp2px(leftoverHeight),
width: vp2px(this.webWidth)
}
};
}
// 开始截图
for (let i = 0; i < snipTimes; i++) {
const curSnip = await componentSnapshot.get('webViewShot');
// 最后一次截图需要特殊处理,去除重复部分
if (i === lastTime) {
await curSnip.crop(cropLeftover);
offCanvasCtx.drawImage(curSnip, 0, this.webHeight * i, this.webWidth, leftoverHeight);
} else {
offCanvasCtx.drawImage(curSnip, 0, this.webHeight * i, this.webWidth, this.webHeight);
}
// 继续滚动
this.controller.scrollBy(0, this.webHeight);
// 延时保证滚动完成
await sleep(200);
}
// 截图后的操作
await this.afterSnapshot();
// 获取pixelMap
this.mergedImage = offCanvasCtx.getPixelMap(0, 0, this.h5Width, this.h5Height);
try {
// 2. 转为JPEG并写入沙箱
const ctx = getContext(this);
const imagePacker = image.createImagePacker();
const arrayBuffer = await imagePacker.packToData(
this.mergedImage,
{ format: 'image/jpeg', quality: 100 } // 平衡质量与大小
);
const sandboxPath = ctx.cacheDir + `/web_fullpage_${Date.now()}.jpg`;
const file = fileIo.openSync(sandboxPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, arrayBuffer);
fileIo.closeSync(file.fd);
// 3. 保存到相册
const helper = photoAccessHelper.getPhotoAccessHelper(ctx);
const sandboxUri = fileUri.getUriFromPath(sandboxPath);
const request = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(ctx, sandboxUri);
await helper.applyChanges(request);
promptAction.showToast({ message: '网页已保存至相册' });
// 拼接之后修改可动画变量
// this.afterGeneratorImage();
} catch (e) {
promptAction.showToast({ message: '保存失败: ' + e.message });
console.error('saveWebViewImage error:', e);
}
}
/**
* 截图前获取尺寸
*/
getWebSize() {
const SCRIPT = '[document.documentElement.scrollWidth, document.documentElement.scrollHeight]';
this.controller.runJavaScriptExt(SCRIPT).then((result) => {
try {
switch (result.getType()) {
case webview.JsMessageType.ARRAY:
this.h5Width = (result.getArray() as number[])[0]; // 单位是vp
this.h5Height = (result.getArray() as number[])[1];
break;
default:
break;
}
} catch (e) {
}
});
}
/**
* 截图开始前的操作。
* - 保存网页当前位置,用于恢复状态
* - 截图当前页面作为遮罩层,避免用户察觉组件的滚动,提高用户体验
* - Web页面滚动到顶部准备开始截图
* - 设置截图后小弹窗的位置,提示用户暂时不要操作,等待截图
* - 开启提示小弹窗
*/
async beforeSnapshot() {
// 保存网页当前位置,用于恢复
this.xOffsetBefore = this.curXOffset;
this.yOffsetBefore = this.curYOffset;
this.h5Height = this.curYOffset + Math.ceil(this.webHeight);
// TODO: 知识点: 使用componentSnapshot.get接口直接获取组件的渲染结果而不需要将屏幕截图
this.webMaskImage = await componentSnapshot.get('webViewShot');
this.webMaskImageZIndex =2;
this.controller.scrollTo(0, 0);
promptAction.showToast({
message: '正在截图,请勿操作...',
duration: 2000
});
// 延时确保已经滚动到了顶部
await sleep(200);
}
/**
* 截图之后的操作。
* - 恢复web页面到截图之前的位置
* - 取消遮罩层
*/
async afterSnapshot() {
this.controller.scrollTo(this.xOffsetBefore, this.yOffsetBefore);
await sleep(200);
this.webMaskImageZIndex = -1;
this.webMaskImage = undefined;
}
/**
* 生成拼接后图片的操作。用于窗口形成移动的动画。
*/
async afterGeneratorImage() {
// 小窗在屏幕中间短暂停留,避免位置突变,无法形成动画
await sleep(200);
// 修改弹窗位置,形成移动动画
}
}
interface RouteParams {
url:string;
title:string;
}
/**
* 异步延时函数。
* @param ms 延时时长,单位 ms。
* @returns Promise 对象,执行回调。
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -41,6 +41,7 @@
"pages/Netease/InterrogationDetailCompPage",
"pages/Netease/PatientSimplyPage",
"pages/Netease/MyOpinionPage",
"pages/PatientsPage/GroupManagementPage"
"pages/PatientsPage/GroupManagementPage",
"pages/WebView/WebPageSnapshot"
]
}