高校周边通 · 国庆特别版: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],越大越相似

官方错误码:

  • 1013100001 Invalid image path.
  • 1013100002 Service abnormal.
  • 1013100003 The 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"理念的最好实践。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐