HarmonyOS Next 实战卡片开发 03

简介: 本文详细介绍了基于 HarmonyOS Next 的卡片开发实战,涵盖从项目创建到功能实现的全流程。首先通过新建项目和服务卡片搭建基础框架,并设置沉浸式体验优化界面。接着实现了首页轮播图功能,包括申请网络权限、初始化数据和构建轮播组件。随后深入讲解了卡片 id 的处理,涉及获取、返回、持久化存储及移除操作,确保卡片与应用间的高效通信。此外,封装了下载图片工具类,支持卡片发起通知获取网络图片,增强功能扩展性。最后实现了卡片同步轮播功能,使首页与卡片轮播状态保持一致。整个流程注重细节,结合实际案例,为开发者提供了全面的参考。

HarmonyOS Next 实战卡片开发 03

在前面两张,我们基本掌握了卡片的使用流程,本章节就通过一个实战来加强对卡片使用的理解。

要完成的案例

image-20241024175411585


PixPin_2024-10-24_17-54-18

新建项目和新建服务卡片

image-20241031111045865


image-20241031111130741

设置沉浸式

entry/src/main/ets/entryability/EntryAbility.ets

image-20241031111326923

首页显示轮播图数据

PixPin_2024-10-31_11-27-05

1. 申请网络权限

entry/src/main/module.json5

image-20241031112238553

2. 新建工具文件 /utils/index.ets

entry/src/main/ets/utils/index.ets

export const swiperInit = () => {
   
  AppStorage.setOrCreate("swiperList", [
    "https://env-00jxhf99mujs.normal.cloudstatic.cn/card/1.webp?expire_at=1729734506&er_sign=e51cb3b4f4b28cb2da96fd53701eaa69",
    "https://env-00jxhf99mujs.normal.cloudstatic.cn/card/2.webp?expire_at=1729734857&er_sign=b2ffd42585568a094b9ecfb7995a9763",
    "https://env-00jxhf99mujs.normal.cloudstatic.cn/card/3.webp?expire_at=1729734870&er_sign=50d5f210191c113782958dfd6681cd2d",
  ]);
  AppStorage.setOrCreate("activeIndex", 0);
};

3. 初始化

entry/src/main/ets/entryability/EntryAbility.ets

image-20241031111803516

4. 页面中使用

entry/src/main/ets/pages/Index.ets


@Entry
@Component
struct Index {
   
  @StorageProp("swiperList")
  swiperList: string[] = []
  @StorageLink("activeIndex")
  activeIndex: number = 0


  build() {
   
    Column() {
   
      Swiper() {
   
        ForEach(this.swiperList, (img: string) => {
   
          Image(img)
            .width("80%")
        })
      }
      .loop(true)
      .autoPlay(true)
      .interval(3000)

      .onChange(index => this.activeIndex = index)
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundImage(this.swiperList[this.activeIndex])
    .backgroundBlurStyle(BlurStyle.Thin)
    .backgroundImageSize(ImageSize.Cover)
    .animation({
    duration: 500 })
  }
}

5. 效果

PixPin_2024-10-31_11-27-05

创建卡片时,获取卡片 id

PixPin_2024-10-31_13-06-50

image-20241031222230532

1. 获取和返回卡片 id

这里解析下为什么要返回 id 给卡片组件,因为后期卡片想要向应用通信时,应用响应数据要根据卡片 id 来响应。

另外 formExtensionAbility 进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在 10 秒,如 10 秒内没有新的

生命周期回调触发则进程自动退出。针对可能需要 10 秒以上才能完成的业务逻辑,建议拉起主应用进行处理,处理完成后使用updateForm通知卡片进行刷新

entry/src/main/ets/entryformability/EntryFormAbility.ets

  onAddForm(want: Want) {
   
    class FormData {
   
      // 获取卡片id
      formId: string = want.parameters!['ohos.extra.param.key.form_identity'].toString();
    }

    let formData = new FormData()
    return formBindingData.createFormBindingData(formData);
  }

2. 接受和显示卡片 id

entry/src/main/ets/widget/pages/WidgetCard.ets

const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
   
  @LocalStorageProp("formId")
  formId: string = ""

  build() {
   
    Row() {
   
      Text(this.formId)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)

  }
}

3. 效果

PixPin_2024-10-31_13-06-50

记录卡片 id,持久化存储

image-20241031223836426

主要流程如下:

  1. 封装持久化存储卡片 id 的工具类
  2. 初始化卡片 id 工具类
  3. 卡片主动上传卡片 id
  4. 应用 Aibility 接收卡片 id
  5. 接收卡片 id 并且持久化
  6. 移除卡片时,删除卡片 id

1. 封装持久化存储卡片 id 的工具类

此时接收到卡片 id 后,需要将卡片 id 持久化存储,避免重新打卡手机时,无法联系到已经创建的卡片

entry/src/main/ets/utils/index.ets

export class FormIdStore {
   
  static key: string = "wsy_collect";
  static dataPreferences: preferences.Preferences | null = null;
  static context: Context | null = null;

  //  初始化
  static init(context?: Context) {
   
    if (!FormIdStore.dataPreferences) {
   
      if (context) {
   
        FormIdStore.context = context;
      }
      FormIdStore.dataPreferences = preferences.getPreferencesSync(
        FormIdStore.context || getContext(),
        {
    name: FormIdStore.key }
      );
    }
  }

  //  获取卡片id 数组
  static getList() {
   
    FormIdStore.init();
    const str = FormIdStore.dataPreferences?.getSync(FormIdStore.key, "[]");
    const list = JSON.parse(str as string) as string[];
    console.log("list卡片", list);
    return list;
  }

  // 新增卡片数组
  static async set(item: string) {
   
    FormIdStore.init();
    const list = FormIdStore.getList();
    if (!list.includes(item)) {
   
      list.push(item);
      FormIdStore.dataPreferences?.putSync(
        FormIdStore.key,
        JSON.stringify(list)
      );
      await FormIdStore.dataPreferences?.flush();
    }
  }

  // 删除元素
  static async remove(item: string) {
   
    FormIdStore.init();
    const list = FormIdStore.getList();
    const index = list.indexOf(item);
    if (index !== -1) {
   
      list.splice(index, 1);
      FormIdStore.dataPreferences?.putSync(
        FormIdStore.key,
        JSON.stringify(list)
      );
      await FormIdStore.dataPreferences?.flush();
    }
  }
}

2. 初始化卡片 id 工具类

  1. onCreate 中初始化

    entry/src/main/ets/entryability/EntryAbility.ets

      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
         
        FormIdStore.init(this.context)
    
  2. onAddForm 中初始化

    onAddForm(want: Want) {
          
      FormIdStore.init(this.context)
    

3. 卡片主动上传卡片 id

利用 watch 监听器来触发上传

entry/src/main/ets/widget/pages/WidgetCard.ets

const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
   
  @LocalStorageProp("formId")
  @Watch("postData")
  formId: string = ""

  // 上传卡片id
  postData() {
   
    postCardAction(this, {
   
      action: 'call',
      abilityName: 'EntryAbility',
      params: {
   
        method: 'createCard',
        formId: this.formId
      }
    });
  }

  build() {
   
    Row() {
   
      Text(this.formId)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)

  }
}

4. 应用 Aibility 接收卡片 id

entry/src/main/ets/entryability/EntryAbility.ets

// callee中要求返回的数据类型
class MyPara implements rpc.Parcelable {
   
  marshalling(dataOut: rpc.MessageSequence): boolean {
   
    return true
  }

  unmarshalling(dataIn: rpc.MessageSequence): boolean {
   
    return true
  }
}

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
   
    FormIdStore.init(this.context)
    // 监听事件
    this.callee.on("createCard", (data: rpc.MessageSequence) => {
   
      // 接收id
      const formId = (JSON.parse(data.readString() as string) as Record<string, string>).formId

      return new MyPara()
    })
  }

5. 接收卡片 id 并且持久化

  1. 开启后台运行权限 "ohos.permission.KEEP_BACKGROUND_RUNNING"

    entry/src/main/module.json5

        "requestPermissions": [
          {
         
            "name": "ohos.permission.INTERNET"
          },
          {
         
            "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
          }
        ],
    
  2. 持久化

     onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
         
        FormIdStore.init(this.context)
        // 监听事件
        this.callee.on("createCard", (data: rpc.MessageSequence) => {
         
          // 接收id
          const formId = (JSON.parse(data.readString() as string) as Record<string, string>).formId
          // 2 持久化
          FormIdStore.set(formId)
    
          return new MyPara()
        })
      }
    

6. 移除卡片时,删除卡片 id

entry/src/main/ets/entryformability/EntryFormAbility.ets

  onRemoveForm(formId: string) {
    FormIdStore.remove(formId)
  }

封装下载图片工具类

将下载图片和拼接卡片需要格式的代码封装到文件中 该工具类可以同时下载多张图片,使用了 Promise.all 来统一接收结果

entry/src/main/ets/utils/CardDonwLoad.ets

1. 封装的工具说明

interface IDownFile {
   
  fileName: string;
  imageFd: number;
}

// 卡片显示 需要的数据结构
export class FormDataClass {
   
  // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
  formImages: Record<string, number>;

  constructor(formImages: Record<string, number>) {
   
    this.formImages = formImages;
  }
}

export class CardDownLoad {
   
  context: Context | null;
  then: Function | null = null;
  imgFds: number[] = [];

  constructor(context: Context) {
   
    this.context = context;
  }

  // 下载单张图片
  async downLoadImage(netFile: string) {
   }

  // 下载一组图片
  async downLoadImages(netFiles: string[]) {
   }

  // 私有下载网络图片的方法
  private async _down(netFile: string) {
   }

  // 手动关闭文件
  async closeFile() {
   
    this.imgFds.forEach((fd) => fileIo.closeSync(fd));
    this.imgFds = [];
  }
}

2. 封装的实现

import {
    http } from "@kit.NetworkKit";
import {
    fileIo } from "@kit.CoreFileKit";

interface IDownFile {
   
  fileName: string;
  imageFd: number;
}

// 卡片显示 需要的数据结构
export class FormDataClass {
   
  // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
  formImages: Record<string, number>;

  constructor(formImages: Record<string, number>) {
   
    this.formImages = formImages;
  }
}

export class CardDownLoad {
   
  context: Context | null;
  then: Function | null = null;
  imgFds: number[] = [];

  constructor(context: Context) {
   
    this.context = context;
  }

  // 下载单张图片
  async downLoadImage(netFile: string) {
   
    const obj = await this._down(netFile);
    let imgMap: Record<string, number> = {
   };
    imgMap[obj.fileName] = obj.imageFd;
    if (!this.imgFds.includes(obj.imageFd)) {
   
      this.imgFds.includes(obj.imageFd);
    }
    return new FormDataClass(imgMap);
  }

  // 下载一组图片
  async downLoadImages(netFiles: string[]) {
   
    let imgMap: Record<string, number> = {
   };

    const promiseAll = netFiles.map((url) => {
   
      const ret = this._down(url);
      return ret;
    });
    const resList = await Promise.all(promiseAll);
    resList.forEach((v) => {
   
      imgMap[v.fileName] = v.imageFd;
      if (!this.imgFds.includes(v.imageFd)) {
   
        this.imgFds.includes(v.imageFd);
      }
    });

    return new FormDataClass(imgMap);
    // return resList.map(v => `memory://${v.fileName}`)
  }

  // 私有下载网络图片的方法
  private async _down(netFile: string) {
   
    let tempDir = this.context!.getApplicationContext().tempDir;
    let fileName = "file" + Date.now();
    let tmpFile = tempDir + "/" + fileName;

    let httpRequest = http.createHttp();
    let data = await httpRequest.request(netFile);
    if (data?.responseCode == http.ResponseCode.OK) {
   
      let imgFile = fileIo.openSync(
        tmpFile,
        fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE
      );

      await fileIo.write(imgFile.fd, data.result as ArrayBuffer);

      const obj: IDownFile = {
   
        fileName,
        imageFd: imgFile.fd,
      };
      // setTimeout(() => {
   
      // }, 0)
      // fileIo.close(imgFile);
      httpRequest.destroy();
      return obj;
    } else {
   
      httpRequest.destroy();
      return Promise.reject(null);
    }
  }

  // 手动关闭文件
  async closeFile() {
   
    this.imgFds.forEach((fd) => fileIo.closeSync(fd));
    this.imgFds = [];
  }
}

卡片发起通知,获取网络图片

PixPin_2024-10-31_20-52-33

image-20241031224019379

  1. 准备好卡片代码,用来接收返回的网络图片数据
  2. 应用 Ability 接收卡片通知,下载网络图片,并且返回给卡片

1. 准备好卡片代码,用来接收返回的网络图片数据

const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
   
  // 用来显示图片的数组
  @LocalStorageProp("imgNames")
  imgNames: string[] = []
  // 卡片id
  @LocalStorageProp("formId")
  @Watch("postData")
  formId: string = ""
  // 当前显示的大图 -  和 应用-首页保持同步
  @LocalStorageProp("activeIndex")
  activeIndex: number = 0

  postData() {
   
    postCardAction(this, {
   
      action: 'call',
      abilityName: 'EntryAbility',
      params: {
   
        method: 'createCard',
        formId: this.formId
      }
    });
  }

  build() {
   
    Row() {
   
      ForEach(this.imgNames, (url: string, index: number) => {
   
        Image(url)
          .border({
    width: 1 })
          .layoutWeight(this.activeIndex === index ? 2 : 1)
          .height(this.activeIndex === index ? "90%" : "60%")
          .borderRadius(this.activeIndex === index ? 12 : 5)
          .animation({
    duration: 300 })
      })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)
    .backgroundImage(this.imgNames[this.activeIndex])
    .backgroundBlurStyle(BlurStyle.Thin)
    .backgroundImageSize(ImageSize.Cover)
    .animation({
    duration: 300 })
  }
}

2. 应用 Ability 接收卡片通知,下载网络图片,并且返回给卡片

entry/src/main/ets/entryability/EntryAbility.ets

// callee中要求返回的数据类型

class MyPara implements rpc.Parcelable {
   
  marshalling(dataOut: rpc.MessageSequence): boolean {
   
    return true;
  }

  unmarshalling(dataIn: rpc.MessageSequence): boolean {
   
    return true;
  }
}

export default class EntryAbility extends UIAbility {
   
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
   
    // 监听事件
    this.callee.on("createCard", (data: rpc.MessageSequence) => {
   
      // 接收id
      const formId = (
        JSON.parse(data.readString() as string) as Record<string, string>
      ).formId;
      // 持久化
      FormIdStore.set(formId);

      class FormData {
   
        imgName?: string[] = [];
        activeIndex?: number = AppStorage.get("activeIndex")!;
      }

      const formInfo = formBindingData.createFormBindingData(new FormData());
      // 先响应空数据 等待网络图片下载完毕后,再响应网络图片数据
      formProvider.updateForm(formId, formInfo);
      const cardDownLoad = new CardDownLoad(this.context);
      cardDownLoad
        .downLoadImages(AppStorage.get("swiperList") as string[])
        .then((ret) => {
   
          const urls = Object.keys(ret.formImages).map((v) => `memory://${
     v}`);
          // 返回卡片数组
          class CimgNames {
   
            imgNames: string[] = urls;
            formImages: Record<string, number> = ret.formImages;
          }

          const formInfo = formBindingData.createFormBindingData(
            new CimgNames()
          );
          formProvider.updateForm(formId, formInfo);
          //   关闭文件
          cardDownLoad.closeFile();
        });

      // 临时处理、防止报错
      return new MyPara();
    });
  }
}

3. 效果

PixPin_2024-10-31_20-52-33

卡片同步轮播

image-20241031224212664

该功能主要是首页在图片轮播时,通知所有的卡片同时更新

entry/src/main/ets/pages/Index.ets

1. 监听轮播图 onChange 事件,设置当前显示的下标

      Swiper() {
   
        ForEach(this.swiperList, (img: string) => {
   
          Image(img)
            .width("80%")
        })
      }
      .loop(true)
      .autoPlay(true)
      .interval(3000)
      .onChange(index => this.activeIndex = index)

2. 监听下标的改变,通知持久化存储中所有的卡片进行更新

  @StorageLink("activeIndex")
  @Watch("changeIndex")
  activeIndex: number = 0

  // 通知所有卡片一并更新
  changeIndex() {
   
    const list = FormIdStore.getList()
    const index = this.activeIndex
    list.forEach(id => {
   
      class FdCls {
   
        activeIndex: number = index
      }

      const formInfo = formBindingData.createFormBindingData(new FdCls())
      formProvider.updateForm(id, formInfo)
    })
  }

3. 效果

PixPin_2024-10-31_22-18-40

总结

FormExtensionAbility 进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在 10 秒,如 10 秒内没有新的

生命周期回调触发则进程自动退出。针对可能需要 10 秒以上才能完成的业务逻辑,建议拉起主应用进行处理,处理完成后使用updateForm通知卡片进行刷

新。

1. 项目开发流程

  1. 新建项目与服务卡片:创建新的项目和服务卡片,为后续开发搭建基础框架。
  2. 设置沉浸式体验:在EntryAbility.ets中进行相关设置,优化用户视觉体验。

2. 首页轮播图数据显示

  1. 申请网络权限:在module.json5中申请,为数据获取做准备。
  2. 新建工具文件:在/utils/index.ets中创建swiperInit函数,用于初始化轮播图数据,包括设置轮播图列表和初始索引。
  3. 初始化操作:在EntryAbility.ets中进行初始化。
  4. 页面使用:在Index.ets中构建轮播图组件,通过SwiperForEach等实现轮播效果,轮播图可自动播放、循环,并能响应索引变化。

3. 卡片 id 的处理

  1. 获取与返回卡片 id:在EntryFormAbility.etsonAddForm函数中获取卡片 id,并返回给卡片组件。原因是后期卡片向应用通信时,应用需根据卡片 id 响应,同时注意formExtensionAbility进程的后台限制。
  2. 接受与显示卡片 id:在WidgetCard.ets中接受并显示卡片 id。
  3. 卡片 id 的持久化存储
    • 封装工具类:在/utils/index.ets中封装FormIdStore类,实现初始化、获取卡片 id 列表、新增和删除卡片 id 等功能。
    • 初始化工具类:在EntryAbility.etsonCreateonAddForm中初始化。
    • 卡片主动上传:在WidgetCard.ets中利用watch监听器触发上传卡片 id。
    • 应用接收与持久化:在EntryAbility.ets中接收卡片 id 并持久化,同时需开启后台运行权限。
    • 移除卡片时处理:在EntryFormAbility.etsonRemoveForm中删除卡片 id。

4. 图片相关操作

  1. 封装下载图片工具类:在CardDonwLoad.ets中封装,包括下载单张或一组图片的功能,以及手动关闭文件功能,涉及网络请求和文件操作。
  2. 卡片发起通知获取网络图片
    • 卡片准备接收数据:在WidgetCard.ets中准备接收网络图片数据的代码,包括显示图片数组、卡片 id 等相关变量和操作。
    • 应用处理与返回数据:在EntryAbility.ets中接收卡片通知,下载网络图片并返回给卡片,先响应空数据,下载完成后再更新卡片数据。

5. 卡片同步轮播功能

  1. 监听轮播图 onChange 事件:在Index.ets中通过Swiper组件的onChange事件设置当前显示下标。
  2. 通知卡片更新:在Index.ets中监听下标改变,通知持久化存储中的所有卡片更新,实现首页与卡片轮播同步。
目录
相关文章
|
6天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
1640 116
|
7天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1115 5
|
13天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1954 9
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
7天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
539 112
|
19天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
2757 4
|
11天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
730 111
|
21天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2653 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
7天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
5天前
|
人工智能 API 开发工具
2026 零基础本地 AI 漫剧完整实操教程(8G 笔记本显卡可用|附可直接复制命令与代码)
本方案提供完全离线、本地运行的漫剧全自动制作流程:RTX3060/4050 8G显卡即可驱动,涵盖Qwen写分镜→ComfyUI统一角色绘图→LTX2.3图生微动画→Qwen3-TTS本地配音→FFmpeg自动合成,全程无水印、免API、不限次。专为低显存优化,解决变脸、闪烁、爆内存三大痛点。(239字)

热门文章

最新文章