163邮箱邮件发送
代码链接:https://pan.wo.cn/s/1J1c4w90901 提取码:1Spc
参考博客:https://developer.huawei.com/consumer/cn/blog/topic/03148160059378015
在module.json5配置文件加上对权限的声明:
"requestPermissions" : [ { "name" : "ohos.permission.INTERNET" } ]
import socket from '@ohos.net.socket' ; import util from '@ohos.util' ; interface LocalAddress { address : string ; family : number ; } interface ServerAddress { address : string ; port : number ; family : number ; } interface MessageValue { message : ArrayBuffer ; } interface SecureOptions { protocols : socket.Protocol []; cipherSuite : string ; } interface ConnectOptions { address : ServerAddress ; secureOptions : SecureOptions ; } let tlsSocket : socket.TLSSocket | null = null ;@Entry @Component struct Index { bindLocal : boolean = false isServerResponse : boolean = false @State serverAddr : string = "smtp.163.com" @State serverPort : number = 465 @State userName : string = "staseaiot@163.com" @State passwd : string = "DAtTEFxcvXt76ByV" @State rcptList : string = "1970884166@qq.com" @State mailTitle : string = "意见反馈" @State mailFrom : string = "staseaiot@163.com" @State mailContent : string = "This is greeting from Harmony OS" @State canSend : boolean = false build ( ) { Row () { Column () { Text ("邮件发送客户端(163邮箱 - 465 TLS)" ) .fontSize (14 ) .fontWeight (FontWeight .Bold ) .width ('100%' ) .textAlign (TextAlign .Center ) .padding (10 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("邮箱服务器地址:" ) .width (120 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ text : this .serverAddr }) .onChange ((value ) => { this .serverAddr = value }) .width (110 ) .fontSize (12 ) .flexGrow (1 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("邮箱服务器端口:" ) .width (120 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ text : this .serverPort .toString () }) .type (InputType .Number ) .onChange ((value ) => { this .serverPort = parseInt (value) }) .width (110 ) .fontSize (12 ) .flexGrow (1 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("邮箱用户名:" ) .width (90 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ text : this .userName }) .onChange ((value ) => { this .userName = value }) .width (110 ) .fontSize (12 ) .flexGrow (1 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("登录密码(授权码):" ) .width (130 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ text : this .passwd }) .onChange ((value ) => { this .passwd = value }) .width (100 ) .fontSize (12 ) .flexGrow (1 ) Button ("登录" ) .onClick (() => { this .login () }) .width (70 ) .fontSize (14 ) .flexGrow (0 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("收件人邮箱:" ) .width (90 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ placeholder : "多个使用逗号分隔" , text : this .rcptList }) .onChange ((value ) => { this .rcptList = value }) .width (110 ) .fontSize (12 ) .flexGrow (1 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("标题:" ) .width (50 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ text : this .mailTitle }) .onChange ((value ) => { this .mailTitle = value }) .width (110 ) .fontSize (12 ) .flexGrow (1 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , alignItems : ItemAlign .Center }) { Text ("发件人邮箱:" ) .width (90 ) .fontSize (14 ) .flexGrow (0 ) TextInput ({ text : this .mailFrom }) .onChange ((value ) => { this .mailFrom = value }) .width (110 ) .fontSize (12 ) .flexGrow (1 ) }.width ('100%' ).padding (5 ) Flex ({ justifyContent : FlexAlign .Start , direction : FlexDirection .Column , alignItems : ItemAlign .Center }) { Text ("邮件内容:" ) .width ('100%' ) .fontSize (14 ) TextArea ({ text : this .mailContent }) .onChange ((value ) => { this .mailContent = value }) .width ('100%' ) .height (120 ) .fontSize (12 ) Button ("发送" ) .enabled (this .canSend ) .onClick (() => { this .sendMail () }) .width (80 ) .fontSize (14 ) } .flexGrow (1 ) .width ('100%' ) .padding (5 ) .height ('100%' ) } .width ('100%' ) .justifyContent (FlexAlign .Start ) .height ('100%' ) .padding (10 ) } .height ('100%' ) } createNewSocket ( ) { if (tlsSocket) { try { tlsSocket.close (); } catch (e) {} } tlsSocket = socket.constructTLSSocketInstance (); this .bindLocal = false ; } async bindSocket ( ) { this .createNewSocket (); try { let localAddress : LocalAddress = { address : "0.0.0.0" , family : 1 }; await tlsSocket!.bind (localAddress); console .log ("C: bind success" ); this .bindLocal = true ; } catch (e) { console .error (`C: bind fail - ${e.message || e} ` ); } tlsSocket!.on ("message" , (value : MessageValue ) => { this .isServerResponse = true ; let msg = buf2String (value.message ); console .log (`S: ${msg} ` ); }); tlsSocket!.on ("error" , (err ) => { console .error (`E: Socket 错误 - ${err.message || err.code || '未知 TLS 错误' } ` ); console .error ("详细错误对象:" , err); this .cleanupSocket (); }); tlsSocket!.on ("close" , () => { console .log ("C: 连接已被服务器断开(可能是空闲超时)" ); console .log ("C: 发送按钮已禁用,请重新点击'登录'或'发送'来重连" ); this .canSend = false ; this .cleanupSocket (); }); } cleanupSocket ( ) { if (tlsSocket) { try { tlsSocket.close (); } catch (e) {} tlsSocket = null ; } this .bindLocal = false ; this .isServerResponse = false ; } async login ( ) { try { this .cleanupSocket (); await this .bindSocket (); let serverAddress : ServerAddress = { address : this .serverAddr , port : this .serverPort , family : 1 }; const secureOptions : SecureOptions = { protocols : [socket.Protocol .TLSv12 , socket.Protocol .TLSv13 ], cipherSuite : 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256' }; const connectOptions : ConnectOptions = { address : serverAddress, secureOptions : secureOptions }; console .log (`C: 正在连接 ${this .serverAddr} :${this .serverPort} (TLS)...` ); await tlsSocket!.connect (connectOptions); console .log ("C: TLS 连接成功!" ); await this .wait4ServerResponse (10000 ); if (!this .isServerResponse ) { throw new Error ("未收到 220 欢迎消息" ); } await this .exeCmdAndWait4Response ("EHLO harmonyos.next" ); await this .exeCmdAndWait4Response ("AUTH LOGIN" ); let loginName = string2Base64 (this .userName ); await this .exeCmdAndWait4Response (loginName); let passWd = string2Base64 (this .passwd ); await this .exeCmdAndWait4Response (passWd); if (this .isServerResponse ) { this .canSend = true ; console .log ("C: 登录成功!(235 Authentication successful)" ); console .log ("C: 可以开始发送邮件" ); } else { throw new Error ("登录失败,请检查邮箱/授权码" ); } } catch (e) { console .error (`C: 登录失败 - ${e.message || e} ` ); this .cleanupSocket (); } } async sendMail ( ) { if (!this .canSend || !tlsSocket) { console .log ("C: 连接不可用,正在自动尝试重连..." ); await this .login (); if (!this .canSend ) return ; } try { console .log ("C: 开始发送邮件..." ); await this .exeCmdAndWait4Response (`MAIL FROM:<${this .mailFrom} >` ); let rcptMails = this .rcptList .split (',' ); for (let rcpt of rcptMails) { rcpt = rcpt.trim (); if (rcpt) await this .exeCmdAndWait4Response (`RCPT TO:<${rcpt} >` ); } await this .exeCmdAndWait4Response ("DATA" ); let mailBody = `From: ${this .mailFrom} \r\n` ; mailBody += `To: ${this .rcptList} \r\n` ; mailBody += `Subject: =?utf-8?B?${string2Base64(this .mailTitle)} ?=\r\n` ; mailBody += `Date: ${new Date ().toUTCString()} \r\n` ; mailBody += `Content-Type: text/plain; charset="utf-8"\r\n` ; mailBody += `Content-Transfer-Encoding: base64\r\n\r\n` ; mailBody += `${string2Base64(this .mailContent)} \r\n` ; mailBody += ".\r\n" ; const chunkSize = 2048 ; for (let i = 0 ; i < mailBody.length ; i += chunkSize) { const chunk = mailBody.substring (i, i + chunkSize); await tlsSocket!.send (chunk); console .log (`C: [chunk sent, length=${chunk.length} ]` ); } await this .wait4ServerResponse (15000 ); if (!this .isServerResponse ) { throw new Error ("服务器未确认邮件接收" ); } console .log ("C: 邮件发送成功!连接保持打开,可继续发送下一封" ); } catch (e) { console .error (`C: 发送失败 - ${e.message || e} ` ); this .cleanupSocket (); } } async exeCmdAndWait4Response (cmd : string ) { if (!tlsSocket) { throw new Error ("Socket 未初始化" ); } this .isServerResponse = false ; let fullCmd = cmd + "\r\n" ; try { await tlsSocket.send (fullCmd); console .log (`C: ${cmd} ` ); } catch (sendErr) { console .error (`C: 发送命令失败 - ${sendErr.message || sendErr} ` ); throw new Error (sendErr.message || sendErr); } await this .wait4ServerResponse (10000 ); } async wait4ServerResponse (timeoutMs : number = 10000 ) { const start = Date .now (); while (!this .isServerResponse ) { if (Date .now () - start > timeoutMs) { throw new Error (`等待响应超时 (${timeoutMs} ms)` ); } await sleep (100 ); } } } function string2Base64 (src : string ): string { try { let textEncoder = new util.TextEncoder (); let encodeValue = textEncoder.encodeInto (src); let tool = new util.Base64Helper (); return tool.encodeToStringSync (encodeValue); } catch (e) { console .error ("Base64 失败:" , e); return "" ; } } function buf2String (buf : ArrayBuffer ): string { try { let msgArray = new Uint8Array (buf); let textDecoder = util.TextDecoder .create ("utf-8" ); return textDecoder.decodeWithStream (msgArray); } catch (e) { console .error ("解码失败:" , e); return "[解码失败]" ; } } function sleep (time : number ) { return new Promise <void >((resolve ) => setTimeout (resolve, time)); }
天气
代码链接:https://pan.wo.cn/s/1X1A4u98932 提取码:FxuX
位置定位:https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-positioning#section1774418211717
在module.json5中添加网络访问权限:
"requestPermissions" : [ { "name" : "ohos.permission.INTERNET" } ]
utils/HttpUtil.ts
import http from '@ohos.net.http' ;import util from '@ohos.util' ;interface ZlibModule { unzip (data : ArrayBuffer , callback : (err : Error , result : ArrayBuffer ) => void ): void ; } declare function requireNapi (moduleName : string ): ZlibModule ;function isGzipData (data : ArrayBuffer | Uint8Array ): boolean { const uint8Array = data instanceof Uint8Array ? data : new Uint8Array (data); return uint8Array.length >= 2 && uint8Array[0 ] === 0x1F && uint8Array[1 ] === 0x8B ; } async function ungzipData (compressedData : ArrayBuffer ): Promise <string > { return new Promise ((resolve, reject ) => { try { const zlib : ZlibModule = requireNapi ('zlib' ); zlib.unzip (compressedData, (err : Error , result : ArrayBuffer ) => { if (err) { reject (err); return ; } const decoder = new util.TextDecoder ('utf-8' ); const uint8Result = new Uint8Array (result); const output = decoder.decodeWithStream (uint8Result); resolve (output); }); } catch (error) { console .error ('zlib 加载失败' , error); reject (new Error ('zlib 解压失败' )); } }); } export function request<T>(url : string ): Promise <T> { return new Promise <T>((resolve, reject ) => { console .log ('发起请求:' , url); const httpRequest = http.createHttp (); httpRequest.request (url, { method : http.RequestMethod .GET , header : { 'Accept-Encoding' : 'gzip' , } }).then (response => { console .log ('请求响应码:' , response.responseCode ); console .log ('响应数据类型:' , typeof response.result ); if (response.responseCode === 200 ) { try { let dataStr : string ; const processData = ( ) => { try { console .log ('尝试解析 JSON,数据前50字符:' , dataStr.substring (0 , 50 )); const result = JSON .parse (dataStr) as T; console .log ('JSON 解析成功' ); resolve (result); } catch (error) { console .error ('处理响应失败:' , error); console .log ('原始数据前200:' , (response.result as string )?.substring (0 , 200 )); reject (new Error ('处理响应失败: ' + error.message )); } }; if (typeof response.result === 'string' ) { console .log ('收到字符串数据,长度:' , response.result .length ); dataStr = response.result ; processData (); } else if (response.result instanceof ArrayBuffer ) { console .log ('收到 ArrayBuffer 数据,长度:' , response.result .byteLength ); const uint8 = new Uint8Array (response.result ); if (isGzipData (uint8)) { ungzipData (response.result ).then (decompressed => { dataStr = decompressed; processData (); }).catch ((error : Error ) => { reject (new Error ('解压失败: ' + error.message )); return ; }); return ; } else { const decoder = new util.TextDecoder ('utf-8' ); dataStr = decoder.decodeWithStream (uint8); processData (); } } else { reject (new Error ('未知响应数据类型' )); return ; } } catch (error) { console .error ('数据处理失败:' , error); reject (new Error ('数据处理失败: ' + error.message )); } } else { reject (new Error (`请求失败,状态码:${response.responseCode} ` )); } }).catch ((error : Error ) => { console .error ('网络请求失败:' , error); reject (new Error ('网络请求失败: ' + error.message )); }).finally (() => { httpRequest.destroy (); }); }); }
service/WeatherService.ts
import { DailyForecast , RealTimeWeather , WeatherResponse , LocationResponse , RealTimeWeatherResponse , DailyForecastResponse , LocationItem } from "../model/WeatherModel" ; import { request } from "../utils/HttpUtil" ;const API_KEY = 'dc6b637284d547fb9427f5caf4475554' ;const API_HOST = 'https://q53yfrtevq.re.qweatherapi.com' ; export async function getWeatherByCity (city : string ): Promise <WeatherResponse > { try { const baseParams = `location=${encodeURIComponent (city)} &key=${API_KEY} ` ; const locationUrl = `${API_HOST} /geo/v2/city/lookup?${baseParams} ` ; console .log ('请求位置URL:' , locationUrl); const locationRes = await request<LocationResponse >(locationUrl); console .log ('位置响应成功' ); if (!locationRes.location || locationRes.location .length === 0 ) { throw new Error ('未找到该城市的天气数据' ); } const locationId = locationRes.location [0 ].id ; console .log ('找到城市ID:' , locationId, '城市名:' , locationRes.location [0 ].name ); const weatherParams = `location=${locationId} &key=${API_KEY} ` ; const realTimeUrl = `${API_HOST} /v7/weather/now?${weatherParams} ` ; console .log ('请求实时天气URL:' , realTimeUrl); const realTimeRes = await request<RealTimeWeatherResponse >(realTimeUrl); console .log ('实时天气响应成功' ); const dailyUrl = `${API_HOST} /v7/weather/3d?${weatherParams} ` ; console .log ('请求天气预报URL:' , dailyUrl); const dailyRes = await request<DailyForecastResponse >(dailyUrl); console .log ('天气预报响应成功' ); const realTime : RealTimeWeather = { temp : realTimeRes.now .temp , text : realTimeRes.now .text , windDir : realTimeRes.now .windDir , windScale : realTimeRes.now .windScale , humidity : realTimeRes.now .humidity , updateTime : realTimeRes.updateTime }; const dailyForecasts : DailyForecast [] = dailyRes.daily .map ((item : DailyForecast ): DailyForecast => ({ fxDate : item.fxDate , tempMax : item.tempMax , tempMin : item.tempMin , textDay : item.textDay })); console .log ('数据处理完成' ); return { realTime, dailyForecasts }; } catch (error) { console .error ('获取天气失败:' , error); throw new Error (error.message ); } }
model/WeatherModel.ts
export interface RealTimeWeather { temp : string ; text : string ; windDir : string ; windScale : string ; humidity : string ; updateTime : string ; } export interface DailyForecast { fxDate : string ; tempMax : string ; tempMin : string ; textDay : string ; } export interface WeatherResponse { realTime : RealTimeWeather ; dailyForecasts : DailyForecast []; } export interface LocationItem { id : string ; name : string ; } export interface LocationResponse { location : LocationItem []; } export interface RealTimeWeatherResponse { now : RealTimeWeather ; updateTime : string ; } export interface DailyForecastResponse { daily : DailyForecast []; }
pages/WeatherPage.ts
import { DailyForecast , RealTimeWeather } from '../model/WeatherModel' ;import { getWeatherByCity } from '../service/WeatherService' ;@Entry @Component struct WeatherPage { @State private cityName : string = '北京' ; @State private inputCity : string = '' ; @State private realTimeWeather : RealTimeWeather | null = null ; @State private dailyForecasts : DailyForecast [] = []; @State private isLoading : boolean = false ; @State private errorMsg : string = '' ; aboutToAppear ( ) { this .fetchWeatherData (this .cityName ); } build ( ) { Column () { Text ('鸿蒙天气' ) .fontSize (28 ) .fontWeight (FontWeight .Bold ) .margin ({ top : 20 , bottom : 15 }) .alignSelf (ItemAlign .Center ); Row ({ space : 10 }) { TextInput ({ placeholder : '请输入城市名称...' , text : this .inputCity }) .onChange ((value : string ) => { this .inputCity = value; }) .width ('70%' ) .height (45 ) .border ({ width : 1 , radius : 8 , color : '#E5E5E5' }) .padding ({ left : 10 }); Button ('查询' ) .width ('20%' ) .height (45 ) .backgroundColor ('#007DFF' ) .fontColor (Color .White ) .borderRadius (8 ) .onClick (() => { if (this .inputCity .trim ()) { this .fetchWeatherData (this .inputCity .trim ()); } }); } .margin ({ bottom : 20 }) .padding ({ left : 15 , right : 15 }); if (this .isLoading ) { LoadingProgress () .width (40 ) .height (40 ) .margin ({ bottom : 20 }) .alignSelf (ItemAlign .Center ); } if (this .errorMsg ) { Text (this .errorMsg ) .fontSize (14 ) .fontColor ('#FF4D4F' ) .margin ({ bottom : 20 }) .alignSelf (ItemAlign .Center ); } if (this .realTimeWeather ) { Column () { Text (`${this .cityName} 实时天气` ) .fontSize (20 ) .fontWeight (FontWeight .Medium ) .margin ({ bottom : 10 }); Row ({ space : 20 }) { Text (`${this .realTimeWeather.temp} °C` ) .fontSize (48 ) .fontWeight (FontWeight .Bold ); Column () { Text (this .realTimeWeather .text ) .fontSize (18 ) .margin ({ bottom : 5 }); Text (`更新时间:${this .formatTime(this .realTimeWeather.updateTime)} ` ) .fontSize (12 ) .fontColor ('#999' ); } } .margin ({ bottom : 15 }); Grid () { GridItem () { this .buildWeatherInfoItem ('风向' , `${this .realTimeWeather.windDir} ` ); } GridItem () { this .buildWeatherInfoItem ('风力' , `${this .realTimeWeather.windScale} 级` ); } GridItem () { this .buildWeatherInfoItem ('湿度' , `${this .realTimeWeather.humidity} %` ); } } .columnsTemplate ('1fr 1fr 1fr' ) .width ('90%' ) .margin ({ bottom : 30 }); } .padding (20 ) .backgroundColor (Color .White ) .borderRadius (12 ) .shadow ({ radius : 4 , color : '#00000010' , offsetX : 0 , offsetY : 2 }) .width ('90%' ) .alignSelf (ItemAlign .Center ); } if (this .dailyForecasts .length > 0 ) { Text ('未来3天预报' ) .fontSize (18 ) .fontWeight (FontWeight .Medium ) .margin ({ bottom : 10 , top : 20 }) .alignSelf (ItemAlign .Start ) .padding ({ left : 15 }); List ({ space : 10 }) { ForEach (this .dailyForecasts , (item : DailyForecast ) => { ListItem () { Row ({ space : 10 }) { Text (item.fxDate ) .width (80 ) .fontSize (14 ); Text (item.textDay ) .width (60 ) .fontSize (14 ); Text (`↑${item.tempMax} °C` ) .width (50 ) .fontSize (14 ) .fontColor ('#FF4D4F' ); Text (`↓${item.tempMin} °C` ) .width (50 ) .fontSize (14 ) .fontColor ('#007DFF' ); } .padding (15 ) .backgroundColor (Color .White ) .borderRadius (8 ) .width ('100%' ); } }) } .padding ({ left : 15 , right : 15 }) .width ('100%' ); } } .width ('100%' ) .height ('100%' ) .backgroundColor ('#F8F8F8' ); } @Builder private buildWeatherInfoItem (title : string , value : string ) { Column () { Text (title) .fontSize (14 ) .fontColor ('#999' ) .margin ({ bottom : 5 }); Text (value) .fontSize (16 ) .fontWeight (FontWeight .Medium ); } .alignItems (HorizontalAlign .Center ); } private formatTime (timeStr : string ): string { return timeStr.replace ('T' , ' ' ).substring (0 , 16 ); } private async fetchWeatherData (city : string ) { this .isLoading = true ; this .errorMsg = '' ; try { const weatherData = await getWeatherByCity (city); this .cityName = city; this .realTimeWeather = weatherData.realTime ; this .dailyForecasts = weatherData.dailyForecasts ; } catch (error) { this .errorMsg = error.message || '获取天气数据失败,请重试' ; console .error ('获取天气失败:' , error); } finally { this .isLoading = false ; } } }