鴻蒙5.0項目開發——接入有道大模型翻譯
【高心星出品】
項目效果圖
項目功能
-
文本翻譯功能
-
支持文本輸入和翻譯結果顯示
-
使用有道翻譯API進行翻譯
-
支持自動檢測語言(auto)
-
支持雙向翻譯(源語言和目標語言可互換)
-
-
文本操作功能
-
支持文本復制
-
支持文本全選
-
支持長按選擇文本
-
支持滾動查看長文本
-
-
生詞本功能
-
可以將翻譯結果保存到生詞本
-
支持保存選中文本的翻譯
-
支持保存整個翻譯結果
-
記錄保存時間
-
-
用戶界面特點
-
采用上下布局,上方為輸入區,下方為結果顯示區
-
支持實時翻譯
-
提供清晰的視覺反饋
-
支持長按菜單操作
-
-
數據存儲
-
使用鴻蒙系統的數據存儲能力
-
支持生詞本的本地存儲
-
支持歷史記錄保存
-
-
網絡功能
-
集成有道翻譯API
-
支持HTTP請求
-
支持錯誤處理
-
支持數據流式傳輸
-
-
安全特性
-
使用API密鑰進行身份驗證
-
支持數據加密傳輸
-
實現簽名驗證機制
-
-
其他功能
-
支持剪貼板操作
-
提供操作提示(Toast提示)
-
支持文本格式化
-
支持多語言界面
-
大模型翻譯 API 簡介
大型模型翻譯:翻譯的好助手,使用此服務可以完成翻譯、潤色、擴寫等功能。API可以處理各種復雜的語言結構、詞匯和語境,提供高質量的翻譯結果。 同時,可以根據用戶的需 求和偏好進行定制化的翻譯。用戶可以通過調整參數、提供上下文信息或者進行反饋,使翻譯結果更符合個人或特 定領域的要求,從而實現更加精準、個性化的翻譯體驗。
接入有道翻譯過程
-
首先要注冊成為有道智云的開發者并創建應用:https://ai.youdao.com/doc.s#guide,就可以拿到
應用ID
和應用密鑰
。 -
大模型翻譯API HTTPS地址:
https://openapi.youdao.com/llm_trans
- 請求方式:
規則 | 描述 |
---|---|
傳輸方式 | HTTPS |
請求方式 | GET/POST |
字符編碼 | 統一使用UTF-8 編碼 |
請求格式 | 表單 |
響應格式 | text/event-stream |
- 請求參數:
字段名 | 類型 | 含義 | 必填 | 備注 |
---|---|---|---|---|
i | text | 待翻譯文本 | True | 必須是UTF-8編碼,限制5000字符 |
prompt | text | 提示詞 | False | 必須是UTF-8編碼,限制1200字符、400單詞 |
from | text | 源語言 | True | 參考下方支持語言 (可設置為auto) |
to | text | 目標語言 | True | 參考下方支持語言 |
streamType | text | 流式返回類型 | False | 參考下方 流式返回類型 |
appKey | text | 應用ID | True | 可在應用管理 查看 |
salt | text | 隨機字符串,可使用UUID進行生產 | True | uuid (可使用uuid生成) |
sign | text | 簽名 | True | sha256(應用ID+input+salt+curtime+應用密鑰) |
signType | text | 簽名類型 | True | v3 |
curtime | text | 當前UTC時間戳(秒) | True | TimeStamp |
handleOption | text | 處理模式選項 | False | 參考下方 處理模式選項 |
polishOption | text | 潤色選項 | False | 參考下方 潤色選項 |
expandOption | text | 擴寫選項 | False | 參考下方 擴寫選項 |
簽名生成方法如下: signType=v3; sign=sha256(
應用ID
+input
+salt
+curtime
+應用密鑰
); 其中,input的計算方式為:input
=i前10個字符
+i長度
+i后10個字符
(當i長度大于20)或input
=i字符串
(當i長度小于等于20);
- 流式返回類型SSE:
event:begindata:{"requestId":"11","type":"zh-CHS2en"}event:messagedata:{"transFull":null,"transIncre":"The"}event:messagedata:{"transFull":null,"transIncre":" w"}...............event:enddata:{"requestId":"11","type":"zh-CHS2en","eventTokenUsage":{"inputToken":5,"outputToken":7,"totalToken":12}}
網絡請求工具封裝
由于大模型翻譯獲取的是增量的翻譯結果,一次應答只能獲取部分翻譯結果,所以我們發送請求的方式要用requestInStream發起流式請求,然后在 req.on(‘dataReceive’, (data) =>{})中獲取每次返回的SSE結果。
此工具封裝最復雜的就是請求參數的獲取,時間戳curtime要獲取當前系統時間的秒級結果,并且服務器會將服務器時間與發送請求的時間戳進行對比,如果差距超過15分鐘,請求就會失敗,所以要關注一下運行該段代碼設備的時間。
/*** 生成請求參數* @param q 待翻譯的文本* @returns 包含簽名等信息的請求參數對象*/
export function genparm(q: string): reqparam {q = q.trim()let salt = util.generateRandomUUID()let curtime = Math.round(new Date().getTime() / 1000) + ''let param: reqparam = {i: q,q: q,from: 'auto',to: 'auto',appKey: APPKEY,curtime: curtime,signType: 'v3',salt: salt,sign: sign(q, curtime, salt)}return param
}/*** 生成簽名* @param q 待翻譯的文本* @param curtime 當前時間戳* @param salt 隨機字符串* @returns SHA256加密后的簽名*/
function sign(q: string, curtime: string, salt: string) {let str = APPKEY + getinput(q) + salt + curtime + APPSECRETlet result: string = ''try {let mdAlgName = 'SHA256'; // 使用SHA256算法let md = cryptoFramework.createMd(mdAlgName);// 更新數據md.updateSync({ data: new Uint8Array(buffer.from(str, 'utf-8').buffer) });let mdResult = md.digestSync();// 將摘要結果轉換為十六進制字符串result = Array.from(mdResult.data).map(byte => byte.toString(16).padStart(2, '0')).join('');} catch (error) {console.error('SHA256編碼失敗:', error);}return result
}/*** 處理輸入文本* @param q 待處理的文本* @returns 處理后的文本* 如果文本長度大于20,則取前10個字符和后10個字符,中間加上長度*/
function getinput(q: string) {let len = q.lengthlet result: stringif (len <= 20) {result = q} else {let startstr = q.substring(0, 10)let endstr = q.substring(len - 10, len)result = startstr + len + endstr}return result
}/*** 發送LLM翻譯請求* @param q 待翻譯的文本* @param recievedata 接收數據的回調函數* @returns Promise<number> 請求ID*/
export function postllm(q: string, recievedata: (data: string) => void) {let req = http.createHttp()let param = genparm(q)let opt: http.HttpRequestOptions = {method: http.RequestMethod.POST,header: {'Content-Type': 'application/x-www-form-urlencoded'},extraData: `i=${param.i}&q=${param.q}&from=auto&to=auto&appKey=${param.appKey}&curtime=${param.curtime}&signType=v3&salt=${param.salt}&sign=${param.sign}`}let strs: string[] = []req.on('dataReceive', (data) => {// 處理接收到的數據let result = buffer.from(data).toString()console.log('gxxt result ', result)if (!result.endsWith('\n')) {strs.push(result.substring(result.lastIndexOf('\n') + 1))result = result.substring(0, result.lastIndexOf('\n') + 1)} else {if (strs.length > 0) {result = strs.join('') + resultstrs = []}}console.log('gxxt newresult ', result)recievedata(result)})return new Promise<number>((resolve, reject) => {req.requestInStream(BASEURL, opt).then((num) => {resolve(num)}).catch((e: Error) => {reject(e.message)})})
}
流式返回數據的處理
目前鴻蒙還沒有支持解析SSE數據的工具,需要開發者自己封裝,其實就是對于字符串的處理。鑒于SSE的結構,可以按照\n\n雙換行來切割獲取不同的事件類型,然后只處理event:message事件即可。
/*** 解析服務器返回的SSE(Server-Sent Events)數據* @param data 服務器返回的原始數據字符串* @returns 解析后的翻譯結果數組,如果發生錯誤則返回undefined*/export function getResult(data: string) {let ret: string[] = []// 檢查是否包含錯誤信息if (data.lastIndexOf('event:error') !== -1) {return}// 按事件分割數據let events = data.split('\n\n')events.forEach((item: string) => {// 處理消息事件if (item.indexOf('event:message') !== -1) {let data1 = item.split('event:message\n')data1.forEach((itemn: string) => {// 提取JSON數據并解析if (itemn.length > 1 && itemn.indexOf('{') !== -1) {let jsondata = itemn.substring(itemn.indexOf('{'), itemn.indexOf('}') + 1)let res = JSON.parse(jsondata) as resdataret.push(res.transIncre)}})}})return ret}
主界面代碼
這是一個名為"星星翻譯"的鴻蒙應用主界面,界面設計簡潔實用,主要分為三個部分:
頂部是標題欄,顯示"星星翻譯"的應用名稱,采用簡潔的白色背景設計。
中間是核心的翻譯區域,采用上下分欄布局:
-
上方是文本輸入區,用戶可以在這里輸入需要翻譯的內容
-
下方是翻譯結果顯示區,實時顯示翻譯結果
-
兩個區域之間有一個翻譯按鈕,點擊即可執行翻譯操作
-
翻譯結果支持長按選擇文本,可以進行復制、全選等操作
底部是功能操作欄,提供兩個主要功能按鈕:
-
復制按鈕:可以將翻譯結果一鍵復制到剪貼板
-
生詞本按鈕:可以將當前的翻譯內容保存到生詞本中,方便后續復習
/*** 星星翻譯應用主頁面* 提供文本翻譯、復制結果、生詞本記錄等功能*/
import { postllm } from '../utils/HttpUtils';
import { getResult, gettime } from '../utils/StringUtils';
import { contentview } from '../views/contentview';
import { footerview } from '../views/footerview';
import { headerview } from '../views/headerview';
import { pasteboard } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { saveshengci } from '../utils/Dbutils';
import { common } from '@kit.AbilityKit';/*** 翻譯應用主頁面組件*/
@Entry
@Component
struct Index {// 源文本和翻譯結果狀態@State @Watch('clear') sourcetext: string = '' // 源文本,監聽變化@State targettext: string = '' // 翻譯結果private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext // 應用上下文/*** 源文本清空時的監聽函數* 當源文本為空時,清空翻譯結果*/clear() {if (this.sourcetext === '') {this.targettext = ''}}/*** 翻譯結果回調函數* @param data 翻譯返回的數據* @description 處理翻譯結果并更新到目標文本*/cb(data: string) {getResult(data)?.forEach((item) => {this.targettext += item})}/*** 執行翻譯操作* @description * 1. 檢查源文本是否為空* 2. 清空之前的翻譯結果* 3. 調用翻譯API* 4. 處理可能的錯誤*/to: () => void = () => {if (this.sourcetext) {this.targettext = ''postllm(this.sourcetext, this.cb.bind(this)).catch((e: string) => {console.error('gxxt ', e)})} else {AlertDialog.show({title: '提示', message: '請輸入要翻譯的內容', confirm: {value: '確定', action: () => {}}})}}/*** 復制翻譯結果到剪貼板* @description * 1. 檢查是否有翻譯結果* 2. 創建剪貼板數據* 3. 設置到系統剪貼板* 4. 顯示操作結果提示*/cpck: () => void = () => {if (this.targettext !== '') {let pasttext = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.targettext)let jianqieban = pasteboard.getSystemPasteboard()jianqieban.setDataSync(pasttext)promptAction.showToast({ message: '已經復制到剪切板' })} else {promptAction.showToast({ message: '沒有翻譯結果,無法復制到剪切板' })}}/*** 保存到生詞本* @description * 1. 記錄翻譯內容* 2. 記錄原文* 3. 記錄保存時間*/tobeiwanglu: () => void = () => {saveshengci({content: this.targettext,trans: this.sourcetext,time: gettime()}, this.context)}/*** 構建UI界面* @description * 1. 頂部標題欄* 2. 中間內容區域(源文本和翻譯結果)* 3. 底部操作欄(復制和生詞本功能)*/build() {Column() {headerview({ text: '星星翻譯', isleft: false })contentview({ sourcetext: this.sourcetext, targettext: this.targettext, transopt: this.to })footerview({ clipck: this.cpck, tobeiwanglu: this.tobeiwanglu })}.width('100%').height('100%')}
}
子組件contentview核心代碼:
/*** contentview.ets* 翻譯內容視圖組件* 提供文本輸入、翻譯結果顯示、文本選擇、復制、生詞本等功能*/import { promptAction } from "@kit.ArkUI"
import { pasteboard } from "@kit.BasicServicesKit"
import { saveshengci } from "../utils/Dbutils"
import { posttxt } from "../utils/HttpUtils"
import { common } from "@kit.AbilityKit"
import { gettime } from "../utils/StringUtils"/***作者:gxx*時間:2025/5/6 14:51*功能:**/
@Preview
@Component
export struct contentview {// 源文本,用于存儲用戶輸入的待翻譯文本@Link sourcetext: string// 目標文本,用于存儲翻譯結果@Link targettext: string// 獲取UI上下文private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext// 翻譯操作回調函數transopt: () => void = () => {}// 當前選中的文本private selecttext: string = ''// 是否全選狀態@State isquanxuan: boolean = false// 是否顯示菜單@State ismenushow: boolean = truebuild() {Stack() {Column() {// 輸入區域Column() {TextArea({ placeholder: '要翻譯的文本' }).width('100%').height('100%').backgroundColor(Color.Transparent).onChange((value) => {this.sourcetext = value.trim()})}.width('100%').height('45%').padding(10).border({ width: 2, color: '#eee' }).borderRadius({ topLeft: 10, topRight: 10 }).backgroundColor(Color.White)// 翻譯結果區域Column() {Scroll() {Text(this.targettext === '' ? '翻譯結果' : this.targettext).width('100%').copyOption(CopyOptions.InApp).fontWeight(FontWeight.Bolder).backgroundColor(Color.Transparent).fontColor(this.targettext === '' ? Color.Gray : Color.Black)// 綁定長按菜單.bindSelectionMenu(TextSpanType.TEXT, this.genselectmenu(), TextResponseType.LONG_PRESS, {onDisappear: () => {this.ismenushow = truethis.isquanxuan = false}})// 文本選擇變化監聽.onTextSelectionChange((start: number, end: number) => {this.selecttext = this.targettext.substring(start, end)})// 全選狀態控制.selection(this.isquanxuan ? 0 : -1, this.isquanxuan ? this.targettext.length : 0)}.scrollable(ScrollDirection.Vertical)}.width('100%').height('45%').padding(10).backgroundColor('#eee').borderRadius({ bottomLeft: 10, bottomRight: 10 })}.width('100%').height('100%').justifyContent(FlexAlign.SpaceBetween)// 翻譯按鈕SymbolGlyph($r('sys.symbol.reverse_order')).fontSize(35).fontWeight(FontWeight.Bolder).border({ width: 2, radius: 10 }).padding(5).stateStyles({normal: {.backgroundColor(Color.White)},pressed: {.backgroundColor(Color.Gray)}}).onClick(() => {this.transopt()})}.width('100%').height('80%').padding(10)}/*** 生成文本選擇菜單* 包含復制、全選、添加到生詞本等功能*/@Buildergenselectmenu() {Row({ space: 15 }) {// 復制按鈕Text('復制').fontSize(12).onClick(() => {let pasttext = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.selecttext)let jianqieban = pasteboard.getSystemPasteboard()jianqieban.setDataSync(pasttext)promptAction.showToast({ message: '已經復制到剪切板' })this.ismenushow = false})// 全選按鈕(僅在非全選狀態顯示)if (!this.isquanxuan) {Text('全選').fontSize(12).onClick(() => {this.isquanxuan = true})}// 生詞本按鈕Text('生詞本').fontSize(12).onClick(() => {this.ismenushow = falseif (this.isquanxuan) {// 全選狀態下保存整個翻譯結果saveshengci({content: this.targettext,trans: this.sourcetext,time: gettime()}, this.context)} else {// 選中狀態下翻譯選中文本并保存posttxt(this.selecttext).then((value) => {let target = value.query// 拼接好的翻譯結果let source = value.translation.join(',')saveshengci({content: target,trans: source,time: gettime()}, this.context)}).catch((e: Error) => {console.error('gxxt 文本翻譯結果: ', e.message)})}})}.backgroundColor(Color.White).padding(10).borderRadius(20).border({ width: 1 }).visibility(this.ismenushow ? Visibility.Visible : Visibility.None)}
}
生詞本頁面代碼
這是一個生詞本界面,采用簡潔現代的設計風格。界面頂部是標題欄,顯示"生詞本"標題,左側配有返回按鈕,方便用戶返回上一頁面。
主體部分是一個可滾動的生詞列表,每個生詞條目以卡片形式展示,包含兩個主要信息:上方顯示生詞內容,采用較大字號和粗體樣式;下方顯示對應的翻譯內容,使用灰色字體。每個條目右側都有一個箭頭圖標,提示用戶可以點擊查看詳情。
用戶可以通過左滑生詞條目來顯示刪除按鈕,點擊刪除按鈕會彈出確認對話框,防止誤操作。點擊生詞條目會彈出一個詳情對話框,以更大的字體展示完整的生詞內容和翻譯,并顯示保存時間。如果內容較長,對話框支持滾動查看。
/*** shengciben.ets* 生詞本頁面組件* 提供生詞列表展示、詳情查看、刪除等功能*/import { shengci } from '../model/shengci';
import { delbyid, querylimit } from '../utils/Dbutils';
import { common } from '@kit.AbilityKit';
import { headerview } from '../views/headerview';
import { ComponentContent, router } from '@kit.ArkUI';/*** 生成生詞詳情對話框* @param p 對話框參數,包含生詞信息、刪除回調、滾動高度等*/
@Builder
function gendialog(p: param) {Stack() {// 關閉按鈕SymbolGlyph($r('sys.symbol.xmark')).fontSize(20).onClick(() => {p.delck()}).zIndex(2)Column({ space: 10 }) {// 標題Text('生詞詳情').fontSize(25).fontWeight(FontWeight.Bolder)Divider().color(Color.Grey).margin({ top: 10, bottom: 10 })// 內容區域Scroll() {Column({ space: 10 }) {// 生詞內容Text(p.item.content).fontSize(18).fontWeight(FontWeight.Bold)// 翻譯內容Text(p.item.trans).fontSize(14).fontColor(Color.Gray)}.alignItems(HorizontalAlign.Start)}.height(p.scrollheight)Divider().color(Color.Grey).margin({ top: 10, bottom: 10 })// 時間信息Text('時間:').fontSize(18).fontWeight(FontWeight.Bold)Text(p.item.time).fontSize(14).fontColor(Color.Gray)}.width('100%').alignItems(HorizontalAlign.Start)}.width('80%').borderRadius(10).alignContent(Alignment.TopEnd).backgroundColor(Color.White).border({ width: 1 }).padding(10)
}/*** 對話框參數接口*/
interface param {item: shengci, // 生詞信息delck: () => void, // 刪除回調函數scrollheight: Length, // 滾動區域高度
}/*** 生詞本頁面組件*/
@Entry
@Component
struct Shengciben {@State message: string = 'Hello World';@State datas: shengci[] = [] // 生詞列表數據private builder: ComponentContent<param> | null = nullprivate context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext/*** 組件即將出現時加載數據*/aboutToAppear(): void {querylimit(this.context, 1).then((value) => {this.datas = value}).catch((e: Error) => {console.error('gxxt 查詢生詞本錯誤: ', e.message)})}build() {Column() {// 頂部標題欄headerview({text: '生詞本', isright: false, leftck: () => {router.back()}})// 生詞列表List({ space: 5 }) {ForEach(this.datas, (item: shengci, index: number) => {ListItem() {this.genlistitem(item)}.swipeAction({ end: this.genitemend(item.id, index) }) // 左滑顯示刪除按鈕.onClick(() => {// 點擊顯示詳情彈窗let p: param = {item: item,delck: () => {this.getUIContext().getPromptAction().closeCustomDialog(this.builder)},scrollheight: item.content.length + item.trans.length > 150 ? 300 : 'auto'}this.builder = new ComponentContent(this.getUIContext(), wrapBuilder<[param]>(gendialog), p)this.getUIContext().getPromptAction().openCustomDialog(this.builder, {alignment: DialogAlignment.Center})})})}.margin({ top: 5 })}.width('100%').height('100%')}/*** 生成列表項右滑刪除按鈕* @param id 生詞ID* @param index 列表索引*/@Buildergenitemend(id: number, index: number) {Row() {SymbolGlyph($r('sys.symbol.trash_fill')).fontSize(30).fontColor([Color.Red])}.padding({left: 10,top: 5,bottom: 5,right: 10}).border({ width: 1, radius: 10 }).margin(5).onClick(() => {// 刪除確認對話框AlertDialog.show({title: '提示',message: '確定要刪除嗎?',primaryButton: {value: '確定', action: () => {delbyid(id, this.context).then(() => {this.datas.splice(index, 1)})}},secondaryButton: {value: '取消', action: () => {}}})})}/*** 生成列表項內容* @param item 生詞信息*/@Buildergenlistitem(item: shengci) {Row() {Column({ space: 5 }) {// 生詞內容Text(item.content).fontSize(20).fontWeight(FontWeight.Bolder).width('60%').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1)// 翻譯內容Text(item.trans).fontSize(14).fontColor(Color.Gray).width('60%').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1)}Blank()// 右箭頭圖標SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(20)}.width('100%').padding({left: 5,top: 10,bottom: 10,right: 5}).border({ width: 1, radius: 5 })}
}
完整項目代碼:
https://download.csdn.net/download/gao_xin_xing/90892395