高校周边通 · 国庆特别版:HarmonyOS 7 文搜图「一日一景」功能开发实战
·
高校周边通 · 国庆特别版:HarmonyOS 7 文搜图「一日一景」功能开发实战

本文代码基于 HarmonyOS 7 / API 26 Beta2 官方文档中的原始示例撰写,函数名、字段名、错误码均来自官网最新版本(更新时间:2026-09-07)。
官方原文链接:
项目背景:「高校周边通」是面向大学生的校园本地生活应用。2026 年国庆版本(v3.6.0)新增 「一日一景」 功能:用户在校园随手拍的国庆元素照片(红旗、校门灯笼、社团彩旗等),可被端侧 AI 自动建索引,用户只要输入一句话就能从相册里翻出对应照片。本文将完整解析该功能背后的
textSearchImage实现。
一、为什么"高校周边通"要在国庆版本接入文搜图?
国庆七天长假是大学生最活跃的拍摄期:迎新晚会、社团彩排、宿舍团建、city walk 打卡……用户相册会在一周内新增数百张照片。但翻照片的痛点随之而来:
- “那天食堂门口挂的红灯笼在哪张图里?”
- “国庆晚会我和室友的合影是哪一张?”
传统按时间线、文件夹浏览,效率极低。「一日一景」通过 textSearchImage 让用户用一句话搜照片,整个过程在端侧完成,零云端流量,零隐私泄露风险。
二、textSearchImage 官方完整接口
| 接口 | 说明 |
|---|---|
textSearchImage.init(): Promise<boolean> | 初始化分析器 |
textSearchImage.release(): Promise<void> | 释放分析器 |
textSearchImage.insertImage(imagePath: string, scope: string): Promise<boolean> | 将单张图片特征插入 scope |
textSearchImage.search(query: string, scope: string, topKey: number): Promise<ImageObject[]> | 在 scope 内检索 |
textSearchImage.deleteImage(imagePath: string, scope: string): Promise<boolean> | 删除单张图片特征 |
textSearchImage.clearData(): Promise<boolean> | 清空所有 scope |
ImageObject 字段:
imagePath: string // 图片路径
scope: string // 图片作用域
similarity: number // 相似度 [-1, 1],越大越相似
官方错误码:
1013100001Invalid image path.1013100002Service abnormal.1013100003The capability has been updated. Please use the new API.
三、「一日一景」功能完整实现
3.1 模块权限配置
// module.json5
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.READ_IMAGEVIDEO" }
]
}
}
3.2 索引管理服务(一日一景核心模块)
/**
* @file DaySceneIndexService.ets
* @description 高校周边通 · 一日一景 索引服务
*/
import { textSearchImage } from '@kit.CoreVisionKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
const DOMAIN = 0x0000;
const TAG = 'DaySceneIndex';
// 索引域:按"国庆 + 高校"维度隔离,方便节后清理
const SCOPE_DAY_SCENE = 'campus.explorer.dayscene.2026nationalday';
export interface DaySceneHit {
imagePath: string;
scope: string;
similarity: number;
}
export class DaySceneIndexService {
/**
* 应用启动时调用(Ability onCreate)
*/
static async init(): Promise<void> {
try {
const ok = await textSearchImage.init();
hilog.info(DOMAIN, TAG,
`Text search image initialization result: ${ok}`);
} catch (error) {
hilog.error(DOMAIN, TAG,
`Init failed. Code: ${error.code}, message: ${error.message}`);
}
}
/**
* 应用退出时调用
*/
static async release(): Promise<void> {
try {
await textSearchImage.release();
hilog.info(DOMAIN, TAG, 'Text search image released successfully');
} catch (error) {
hilog.error(DOMAIN, TAG,
`Release failed. Code: ${error.code}, message: ${error.message}`);
}
}
/**
* 将相册中的"国庆专题"照片批量索引
* @param photoUris 用户从 photoPicker 选择的 uri 列表
*/
static async batchIndex(photoUris: string[]): Promise<number> {
let successCount = 0;
for (const uri of photoUris) {
// 1. uri 转真实路径(官方要求:必须是沙箱路径,长度 [1,128])
const realPath = await DaySceneIndexService.uriToPath(uri);
if (!realPath) continue;
// 2. 调官方 insertImage
try {
const result = await textSearchImage.insertImage(realPath, SCOPE_DAY_SCENE);
if (result) successCount++;
hilog.info(DOMAIN, TAG, `Insert ${realPath}: ${result}`);
} catch (error) {
const err = error as BusinessError;
hilog.warn(DOMAIN, TAG,
`Insert failed. Code: ${err.code}, message: ${err.message}`);
}
}
return successCount;
}
/**
* 一日一景 - 语义搜索入口
* 用户输入:"操场上的五星红旗"、"食堂门口的灯笼"
*/
static async search(query: string, topKey: number = 30): Promise<DaySceneHit[]> {
try {
const results = await textSearchImage.search(query, SCOPE_DAY_SCENE, topKey);
hilog.info(DOMAIN, TAG, `Search "${query}" count: ${results.length}`);
return results.map(item => ({
imagePath: item.imagePath,
scope: item.scope,
similarity: item.similarity
}));
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG,
`Search failed. Code: ${err.code}, message: ${err.message}`);
return [];
}
}
/**
* 删除某张图片索引(用户在相册中删除时同步触发)
*/
static async delete(imagePath: string): Promise<boolean> {
try {
const result = await textSearchImage.deleteImage(imagePath, SCOPE_DAY_SCENE);
hilog.info(DOMAIN, TAG, `Delete ${imagePath}: ${result}`);
return result;
} catch (error) {
const err = error as BusinessError;
hilog.warn(DOMAIN, TAG,
`Delete failed. Code: ${err.code}, message: ${err.message}`);
return false;
}
}
/**
* 国庆结束后清理索引
*/
static async clearAll(): Promise<boolean> {
try {
const result = await textSearchImage.clearData();
hilog.info(DOMAIN, TAG, `Clear all data: ${result}`);
return result;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG,
`Clear failed. Code: ${err.code}, message: ${err.message}`);
return false;
}
}
/**
* photoAccessHelper uri 转沙箱路径
*/
private static async uriToPath(uri: string): Promise<string | null> {
try {
const file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
const path = fileIo.getFilePathFromUri(file.fd);
await fileIo.close(file);
return path;
} catch {
return null;
}
}
}
3.3 UI 层:国庆主题搜索页
/**
* @file DayScenePage.ets
* @description 高校周边通 · 一日一景 搜索页
*/
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { DaySceneIndexService, DaySceneHit } from '../utils/DaySceneIndexService';
@Entry
@Component
struct DayScenePage {
@State query: string = '';
@State hits: DaySceneHit[] = [];
@State indexedCount: number = 0;
// 国庆快捷检索词
private readonly holidayPresets: string[] = [
'校园里的五星红旗',
'食堂门口的灯笼',
'宿舍阳台的烟花',
'操场的迎新晚会',
'校门外的国庆彩旗'
];
async aboutToAppear(): Promise<void> {
await DaySceneIndexService.init();
}
async aboutToDisappear(): Promise<void> {
await DaySceneIndexService.release();
}
build() {
Column() {
// 国庆 Banner
Row() {
Text('一日一景 · 国庆特辑')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFE600')
Image($r('app.media.national_flag'))
.width(24).height(24)
}
.width('100%')
.padding(16)
.linearGradient({
angle: 90,
colors: [['#FFE60019', 0.0], ['#FFFF0000', 1.0]]
})
// 一键索引按钮
Button('📸 从相册导入国庆照片')
.width('90%')
.height(48)
.backgroundColor('#FFE60019')
.fontColor(Color.White)
.margin({ top: 16 })
.onClick(async () => {
const picker = new photoAccessHelper.PhotoViewPicker();
const opts = new photoAccessHelper.PhotoSelectOptions();
opts.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
opts.maxSelectNumber = 50;
const result = await picker.select(opts);
const count = await DaySceneIndexService.batchIndex(result.photoUris);
this.indexedCount = count;
})
// 索引结果
if (this.indexedCount > 0) {
Text(`已索引 ${this.indexedCount} 张国庆照片`)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 8 })
}
// 搜索框
Row() {
TextInput({ placeholder: '搜一句:灯笼 / 红旗 / 烟花…' })
.layoutWeight(1)
.height(40)
.backgroundColor('#F5F5F5')
.onChange((v: string) => { this.query = v; })
Button('搜索')
.height(40)
.backgroundColor('#FFE60019')
.onClick(() => this.doSearch())
}
.padding(16)
// 国庆快捷词
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.holidayPresets, (preset: string) => {
Text(`#${preset}`)
.fontSize(12)
.fontColor('#FFE60019')
.backgroundColor('#22E60019')
.padding(8)
.borderRadius(16)
.margin(4)
.onClick(() => {
this.query = preset;
this.doSearch();
})
})
}
.padding({ left: 16, right: 16 })
// 命中结果
Grid() {
ForEach(this.hits, (hit: DaySceneHit) => {
GridItem() {
Stack() {
Image(`file://${hit.imagePath}`)
.objectFit(ImageFit.Cover)
.width('100%').height(140)
Text(`${(hit.similarity * 100).toFixed(0)}%`)
.fontColor(Color.White)
.fontSize(11)
.backgroundColor('#99000000')
.padding(4)
.borderRadius(4)
}
}
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding(16)
.layoutWeight(1)
}
.width('100%').height('100%')
.backgroundColor('#FFFFFF')
}
private async doSearch() {
if (!this.query.trim()) return;
this.hits = await DaySceneIndexService.search(this.query, 30);
}
}
四、节后运营策略
textSearchImage.clearData() 会清空所有 scope,所以在节后建议:
// 国庆结束 7 天后,自动清理索引
aboutToDisappear() {
if (this.isNationalDayEnded()) {
DaySceneIndexService.clearAll();
}
}
也可以保留索引但提示用户「索引已过期,是否清理?」,把选择权交给用户。
五、写在最后
「一日一景」是 textSearchImage 在 HarmonyOS 7 上的典型落地场景。它把 AI 搜索能力下沉到端侧,让学生在节日期间既能快速翻照片,又不用担心隐私泄露。这正是 HarmonyOS 7 强调的"Local-first AI"理念的最好实践。
更多推荐



所有评论(0)