当前位置: 首页 > article >正文

HarmonyOS NEXT - 电商App实例四(登录界面)

        登录界面是用户进入App的第一步,因此需要简洁明了,同时保持品牌风格的一致性。如:顶部区域为品牌LOGO展示,增加品牌识别度;中间区域为登录表单,包含输入框和按钮;底部区域为其他登录方式、注册入口和忘记密码相关链接。

        在HarmonyOS中,使用ArkTS-UI框架完成登录界面的设计,会使用到Text组件、Textinput组件、Button组件、Image组件、Link组件、Row和Column布局容器等。数据交互方面,使用@State装饰器记录用户名和密码的表单数据状态,按钮事件和输入框事件的处理函数,则使用到onClick和onChange,确保用户操作能够触发相应的逻辑。

一、界面设计 

        在HarmonyOS中使用ArkTS-UI设计登录界面,将使用以下组件:

1.1 Row和Column布局容器

        使用Row和Column容器完成登录表单的布局。通过配置Row容器的padding属性,使App容器四周留出20像素间距,通过配置Column容器的justifyContent,使内容水平和垂直居中。

        示例代码如下:

@Entry
@Component
struct Login {
  build() {
    RelativeContainer() {
      Row(){
        Column(){

        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

1.2 Image组件

        使用Image组件用于显示品牌Logo。代码如下:

@Entry
@Component
struct Login {
  build() {
    RelativeContainer() {
      Row(){
        Column(){
          // 添加Logo图标
          Image($rawfile('logo.png'))
            .width(80)
        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

1.3 Text组件

        使用Text组件,用于显示Logo文本信息,如登录标题、提示等。示例代码如下:

@Entry
@Component
struct Login {
  build() {
    RelativeContainer() {
      Row(){
        Column(){
          // 添加Logo图标
          Image($rawfile('logo.png'))
            .width(80)
          // 添加标题
          Text('欢迎登录XXX平台')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .padding({ top: 15, bottom: 15})
        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

1.4 TextInput组件

        使用TextInput组件,用于输入用户名或手机号、密码等。同时,使用Column容器(垂直布局)将表单内容包裹起来,并配置space为20,使其内部元素垂直布局元素间距为20。示例代码如下:

@Entry
@Component
struct Login {
  build() {
    RelativeContainer() {
      Row({ space: 20 }){
        Column(){
          // 添加Logo图标
          Image($rawfile('logo.png'))
            .width(80)
          // 添加标题
          Text('欢迎登录XXX平台')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .padding({ top: 15, bottom: 15})
          // 表单输入框
          Column({ space: 20 }){
            TextInput({ placeholder: '请输入用户名/手机号' })
            TextInput({ placeholder: '请输入密码' }).type(InputType.Password)
          }.padding({ top: 20, bottom: 50 })
        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

1.5 Button组件

        使用Button组件,用于登录、注册或忘记密码等功能。示例代码如下:

@Entry
@Component
struct Login {
  build() {
    RelativeContainer() {
      Row({ space: 20 }){
        Column(){
          // 添加Logo图标
          Image($rawfile('logo.png'))
            .width(80)
          // 添加标题
          Text('欢迎登录XXX平台')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .padding({ top: 15, bottom: 15})
          // 表单输入框
          Column({ space: 20 }){
            TextInput({ placeholder: '请输入用户名/手机号' })
            TextInput({ placeholder: '请输入密码' }).type(InputType.Password)
          }.padding({ top: 20, bottom: 50 })
          // 登录按钮
          Button('登 录', { type: ButtonType.Capsule })
            .width('100%')
        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

1.6 链接文本

        使用Link组件,用于注册、忘记密码的链接。用Row容器(水平布局)将文本链接包裹起来,并且文本组件之间的间距设置为20。示例代码如下:

@Entry
@Component
struct Login {
  build() {
    RelativeContainer() {
      Row({ space: 20 }){
        Column(){
          // 添加Logo图标
          Image($rawfile('logo.png'))
            .width(80)
          // 添加标题
          Text('欢迎登录XXX平台')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .padding({ top: 15, bottom: 15})
          // 表单输入框
          Column({ space: 20 }){
            TextInput({ placeholder: '请输入用户名/手机号' })
            TextInput({ placeholder: '请输入密码' }).type(InputType.Password)
          }.padding({ top: 20, bottom: 80 })
          // 登录按钮
          Button('登 录', { type: ButtonType.Capsule })
            .width('100%')
          // 添加 忘记密码 和 注册
          Row({ space: 20 }){
            Text('忘记密码?').fontColor('#1495E7')
            Text('注册').fontColor('#FF0000')
          }.width('100%')
           .justifyContent(FlexAlign.End)
           .padding({ top: 15, right: 10, bottom: 50})
        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

        通过以上组件,就已设计出一个简洁且功能完善的登录界面。

二、界面交互功能

        界面元素已设计完毕,现在则需要设计交互功能,当用户输入内容时,需将填写内容实时赋值给状态变量;如在未填任何内容时,用户点击“登录”按钮,则弹出提示框提示用户。

2.1 状态管理 

        使用@State装饰器管理登录表单的状态,例如用户和密码。示例代码如下:

@Entry
@Component
struct Login {
  @State username: string = '';   // 用户名
  @State password: string = '';   // 密码

 // 略...
}

2.2 更新表单数据

        在输入框上添加onChange事件,当文本内容被修改后,实时将回调函数中内容赋值给状态变量。示例代码如下:

// 表单输入框
Column({ space: 20 }){
  TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
    .onChange((val: string) => {
      this.username = val
    })
  TextInput({ placeholder: '请输入密码', text: this.password })
    .type(InputType.Password)
    .onChange((val: string) => {
      this.password = val
    })

2.3 登录按钮

        在登录按钮上添加onClick事件,当用户点击时,校验用户名/手机号,密码是否正确;如信息有误,弹出信息提示用户;如信息正确,执行登录操作。示例代码如下:

// 表单输入框
Column({ space: 20 }){
  TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
    .onChange((val: string) => {
      this.username = val
    })
  TextInput({ placeholder: '请输入密码', text: this.password })
    .type(InputType.Password)
    .onChange((val: string) => {
      this.password = val
    })
}.padding({ top: 20, bottom: 80 })
// 登录按钮
Button('登 录', { type: ButtonType.Capsule })
  .width('100%')
  .onClick(() => {
    // do something...
    console.log(this.username, this.password)
  })

2.4 弹框组件

        在表单校验前,先定义showMsg函数,用于信息提示功能。提示框使用的是HarmonyOS内置函数promptAction,示例代码如下:

  /**
   * 显示信息
   * @param text
   */
  showMsg(text: string = '') {
    promptAction.showToast({
      message: text,
      duration: 1500,
      alignment: Alignment.Center
    })
  }

        AlertDialog的主要参数说明:

名称说明
message

弹窗内容。

duration提示框显示的时长,单位为毫秒。超出该时长后,提示框会自动关闭。
alignment提示框的显示位置,支持多种对齐方式(如Alignment.Center、Alignment.Top等)。
backgroundColor提示框的背景颜色。
textColor提示框文字的颜色。

2.5 信息校验

        在正式将登录表单数据发送给后台之前,前端必须先做基础校验,验证数据符合要求后,再执行发送请求。在登录界面,定义validateForm函数,用于校验用户输入的表单数据。代码如下:

  /**
   * 校验表单
   */
  validateForm(){
    if (!this.username || !this.password) {
      this.showMsg('请输入用户名或名称')
      return;
    } else if (this.password.length < 6) {
      this.showMsg('密码不能小于6位')
      return;
    }
    // 校验通过,提交数据
    console.log(this.username, this.password)
  }

        此时,如果用户什么也没填写,点击“登录”按钮,会提示用户输入相关内容。如下图:

三、数据交互

        登录界面必须要与后台完成数据交互,才能最终实现登录的功能。通常通过HTTP请求来完成,这里就直接使用HarmonyOS提供的http模块来实现这步。

        在前面两篇中,已讲解过http和axios请求模块,根据自身喜好选择一个来完成项目的数据请求即可。

地址一:HarmonyOS开发 - 电商App实例二( 网络请求http)-CSDN博客

地址二:HarmonyOS开发 - 电商App实例三( 网络请求axios)-CSDN博客

3.1 定义标准接口返回类型

        打开项目的types目录下的http.d.ts文件,在里面定义一个标准接口返回类型。

        代码如下:

/**
 * 接口标准返回格式
 */
interface standardInterfaceResult {
  code: number,
  data: any,
  msg: string
}

3.2 定义登录API函数

        如下图,在项目的api目录,创建login.ts文件,用于定义登录、注册、忘记密码等相关api的请求函数。

        示例代码如下:

import { standardInterfaceResult } from '../types/http'
import { httpRequest } from '../utils/request'

/**
 * 登录Api函数
 */
export const login = async (data) => {
  return await httpRequest.post<standardInterfaceResult>('/login.php', data)
}

3.3 登录请求

        将登录定义的API函数login()引入到登录界面,当用户信息输入校验正确时,执行login()函数并得到响应后,根据后台返回结果做出相应操作。代码如下:

import { login } from '../api/login'
import router from '@ohos.router';

@Entry
@Component
struct Login {
  @State username: string = '';   // 用户名
  @State password: string = '';   // 密码

  /**
   * 显示信息
   * @param text
   */
  showMsg(text: string = '') {
    AlertDialog.show({
      title: '提示',
      message: text
    })
  }

  /**
   * 校验表单
   */
  validateForm(){
    if (!this.username || !this.password) {
      this.showMsg('请输入用户名或名称')
      return;
    } else if (this.password.length < 6) {
      this.showMsg('密码不能小于6位')
      return;
    }
    // 校验通过,提交数据
    login({
      username: this.username,
      password: this.password
    }).then(res => {
      this.showMsg(res.msg)
      // code 为200时,跳转到指定界面
      if (res.code == 200) {
        // 跳转到首页
        setTimeout(() => {
          router.pushUrl({
            url: '/pages/Index'
          })
        }, 1200)
      }
    })
  }

  build() {
    RelativeContainer() {
      Row({ space: 20 }){
        Column(){
          // 略... 

          // 表单输入框
          Column({ space: 20 }){
            TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
              .onChange((val: string) => {
                this.username = val
              })
            TextInput({ placeholder: '请输入密码', text: this.password })
              .type(InputType.Password)
              .onChange((val: string) => {
                this.password = val
              })
          }.padding({ top: 20, bottom: 80 })
          // 登录按钮
          Button('登 录', { type: ButtonType.Capsule })
            .width('100%')
            .onClick(() => {
              // 校验表单
              this.validateForm()
            })

          // 略...

        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        此时,我们随便输入一个用户名(13233332222)和密码(12345),后台校验不通过,同样会返回相应错误信息,提示用户重新操作。如下图:

        现在,我们输入正确的用户名(13200002222)和密码(123456),再来看下结果。如下图:

        如下图,控制台输出登录接口的响应数据,其中包含了用户的信息和登录访问令牌。

3.4 本地信息存储        

        登录成功后,可以将登录接口响应的用户信息存储到本地,以便下次自动填充或保持登录状态;这里,可以使用dataPreference模块,将用户信息和访问令牌缓存在本地。

        在整个项目中,如下单、订单信息、用户信息等相关数据,都需要在登录状态下才能访问;并且,这些需要权限的api请求,要在http的header配置中添加访问令牌(即登录成功时,返回的accesstoken数据)。

        所以,我们要在utils目录中,定义一个localStorage.ts文件,用于全局存储用户和访问令牌数据,并有共享读取功能。之前写过几篇本地持久化文章,大家可以参考一下,地址:HarmonyOS开发 - 本地持久化之实现LocalStorage实例_鸿蒙开发本地缓存-CSDN博客。

3.4.1  localStorage.ts文件

        代码如下:

import common from '@ohos.app.ability.common'
import preferences from '@ohos.data.preferences'

/**
 * 判断字符串是否为JSON对象
 */
export const isJsonObject = (value: string) : boolean => {
  try {
    const parseStr = JSON.parse(value)
    return 'object' === typeof parseStr && null !== parseStr
  } catch (e) {
    console.log('testTag', e)
    return false
  }
}

// 定义存储值类型
type valueType = string | number | boolean
// 定义json对象存储类型
type dataType = { value: valueType | object, expire: number }

/**
 * 定义LocalStorage类
 */
export class LocalStorage {
  private preference: preferences.Preferences // 用户首选项实例对象

  // 定义初始化函数
  initial(context: common.UIAbilityContext): void {
    // 这里将UIAbility中应用上下文的moduleName作用为实例名称,即该项目的application
    preferences.getPreferences(context, context.abilityInfo.moduleName).then(preference => {
      this.preference = preference
      console.log('testTag', 'success~')
    }).catch(e => {
      console.log('testTag error', e)
    })
  }
  /**
   * 定义增加函数
   * @param key
   * @param value
   * @param expire
   */
  put(key: string, value: valueType | object, expire?: Date): void {
    // 定义存储Json格式对象
    const data : dataType = {
      value,      // 存储内容
      expire : (expire ? expire.getTime() : -1)   // 如果失效时间存在,将其转换为时间戳,否则传入-1
    }
    let dataStr: string = '';
    try {
      dataStr = JSON.stringify(data)    // 当数据转换成功,将其存储
      console.log('testTag', dataStr)
    } catch (e) {
      console.log('testTag error', e)
      return
    }
    this.preference.put(key, dataStr).then(() => this.preference.flush()).catch(e => {
      console.log('testTag error', e)
    })
  }

  /**
   * 定义获取对应key数据
   * @param key
   */
  async getValue(key: string): Promise<valueType | object> {
    // 首页判断key值是否存在,不存在返回空
    if(!this.preference.has(key)) {
      return Promise.resolve(null)
    }
    let value = (await this.preference.get(key, '')) as valueType
    // 判断如果为字符串类型数据,并且为JSON对象格式数据,将其转换为对象
    if('string' === typeof value && isJsonObject(value)) {
      try {
        const data: dataType = JSON.parse(value)
        console.log('testTag', data.expire, Date.now(), data.expire < Date.now())
        // 如果当前存储内容无时效性,或者在时效期内,都直接返回
        if(data.expire === -1 || data.expire > Date.now()) {
          return Promise.resolve(data.value)
        }
        // 如果已失效,将其信息删除
        else {
          this.preference.delete(key)
        }
      } catch (e) {
        console.log('testTag error', e)
        return Promise.resolve(null)      // 如果转换出错,返回null
      }
    }
    // 通过Promise异步回调将结果返回(如果内容不为JSON格式对象,或者过了时效期,返回null)
    return Promise.resolve(null)
  }

  /**
   * 更新数据
   * @param key
   * @param value
   */
  async update(key: string, value: valueType){
    try {
      const preValue = await this.getValue(key)
      if(preValue != value) {
        this.put(key, value)
      }
    } catch (e) {
      console.log('testTag error', e)
    }
  }
  /**
   * 定义移除函数
   * @param key
   */
  remove(key: string): void {
    this.preference.delete(key).then(() => this.preference.flush()).catch(e => {
      console.log('testTag error', e)
    })
  }
  /**
   * 定义清除所有数据函数
   */
  clearAll(): void {
    this.preference.clear().then(() => this.preference.flush()).catch(e => {
      console.log('testTag error', e)
    })
  }
}
/**
 * 实例LocalStorage
 */
const localStorage = new LocalStorage()

/**
 * 导出localStorage单例对象
 */
export default localStorage as LocalStorage

        功能参数说明 :

字段说明
initial初始化LocalStorage函数,在App打开时,需要将UIAbilityContext传入
put

该方法用于添加缓存数据,

key:缓存数据键名

value: 要缓存的数据

expire:需要缓存的时间(注意:是Date类型数据)

getValue通过key获取缓存的数据
update通过key更新被缓存的数据
remove通过key移除指定的缓存数据
clearAll清空该实例context下的所有缓存数据

3.4.2 UIAbility中初始化

        打开文件目录:src/main/ets/entryability/EntryAbility.ets,找到UIAbility中的onCreate函数,在其内部初始化。

        代码如下:

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import LocalStorage from '../utils/localStorage'

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.context.getApplicationContext().setColorMode(
        ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
    );
    // 初始化LocalStorage
    LocalStorage.initial(this.context)
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  // 略...
}

3.4.3 本地模拟器

        注意,本地缓存使用的是@ohos.data.preferences,必须在本地模拟器中才能生效;在“预览”中无法缓存,故登录后获取不到缓存数据,所以必须在模拟器中演示和操作。如下图的步骤,打开本地模拟器:

3.4.4 定义获取信息函数

        接下来,我们先在首页中,添加获取登录信息的方法;如果登录信息不存在,则跳转登录界面;如果存在,则显示登录信息。首页代码如下:

import LocalStorage from '../utils/localStorage';
import router from '@ohos.router';

// 定义用户数据类型
interface userInfo {
  name: string,
  avatar: string
}

@Entry
@Component
struct Index {
  @State bannerMessage: string = '';

  /**
   * 获取用户信息
   */
  async getUserInfo(){
    const userInfo = await LocalStorage.getValue('userInfo') as userInfo
    const accessToken = await LocalStorage.getValue('accessToken')
    console.log('tag', userInfo, accessToken)
    if (!userInfo || !accessToken) {
      router.pushUrl({
        url: 'pages/Login'
      })
    }
    this.bannerMessage = `userInfo: ${userInfo.name} accessToken:${accessToken}`;
  }

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        Text(this.bannerMessage)

        Button('登录令牌').onClick(() => {
          this.getUserInfo()
        })
      }.width('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        模拟器首页如下图:

        点击“登录令牌”,查看控制台输出结果为null,表示用户信息未获取到,故跳转至登录界面。

 3.4.5 缓存数据

        上述工作准备完成后,就可以将登录接口返回的响应数据,通过封装的LocalStorage功能来缓存起来了。

        在登录成功后,使用LocalStorage实例对象,将数据缓存一天。代码如下:

import { login } from '../api/login'
import router from '@ohos.router';
import { promptAction } from '@kit.ArkUI';
import LocalStorage from '../utils/localStorage'

@Entry
@Component
struct Login {
  @State username: string = '';   // 用户名
  @State password: string = '';   // 密码

  /**
   * 显示信息
   * @param text
   */
  showMsg(text: string = '') {
    promptAction.showToast({
      message: text,
      duration: 1500,
      alignment: Alignment.Center
    })
  }

  /**
   * 校验表单
   */
  validateForm(){
    if (!this.username || !this.password) {
      this.showMsg('请输入用户名或名称')
      return;
    } else if (this.password.length < 6) {
      this.showMsg('密码不能小于6位')
      return;
    }
    console.log(this.username, this.password)
    // 校验通过,提交数据
    login({
      username: this.username,
      password: this.password
    }).then(res => {
      this.showMsg(res.msg)
      console.log('res', JSON.stringify(res.data))
      // code 为200时,跳转到指定界面
      if (res.code == 200) {
        const date = new Date()
        date.setDate(date.getDate() + 1)  // 从今天开始,往后缓存一天
        // 缓存数据
        LocalStorage.put('userInfo', res.data['userInfo'], date)
        LocalStorage.put('accessToken', res.data['accessToken'], date)
        // 跳转到首页
        setTimeout(() => {
          router.pushUrl({
            url: 'pages/Index'
          })
        }, 1200)
      }
    }).finally(() => {
      this.username = ''
      this.password = ''
    })
    //
  }

  build() {
    RelativeContainer() {
      Row({ space: 20 }){
        Column(){
          // 添加Logo图标
          Image($rawfile('logo.png'))
            .width(80)
          // 添加标题
          Text('欢迎登录XXX平台')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .padding({ top: 15, bottom: 15})
          // 表单输入框
          Column({ space: 20 }){
            TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
              .onChange((val: string) => {
                this.username = val
              })
            TextInput({ placeholder: '请输入密码', text: this.password })
              .type(InputType.Password)
              .onChange((val: string) => {
                this.password = val
              })
          }.padding({ top: 20, bottom: 80 })
          // 登录按钮
          Button('登 录', { type: ButtonType.Capsule })
            .width('100%')
            .onClick(() => {
              // 校验表单
              this.validateForm()
            })
          // 添加 忘记密码 和 注册
          Row({ space: 20 }){
            Text('忘记密码?').fontColor('#1495E7')
            Text('注册').fontColor('#FF0000')
          }.width('100%')
           .justifyContent(FlexAlign.End)
           .padding({ top: 15, right: 10, bottom: 50})
        }.width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }.width('100%')
       .padding(20)
    }
    .height('100%')
    .width('100%')
  }
}

        当登录信息不存在时,跳转至登录界面,输入用户信息,完成登录操作。如下图:

        点击“登录”,查看控制台输出。LocalStorage缓存数据控制正常输出信息,表示缓存成功。如下图:

3.4.6 获取用户信息

        登录成功后,会自动跳转至首页,接下来我们重新获取用户信息,查看结果。如下图:

        当点击“登录令牌”,用户名称和访问令牌则成功获取到,并显示在界面上。

         这里,登录功能就讲完了,如果你有更好的方法,欢迎随时沟通交流!


http://www.kler.cn/a/586211.html

相关文章:

  • Qt:绘图API
  • 20250314-vue-Props3
  • Free QWQ - 世界首个免费无限制分布式 QwQ API
  • Milvus 中常见相似度度量方法
  • 考研复习,主动学习”与“被动接收”的结合之道
  • Android自动化测试工具
  • 高级java每日一道面试题-2025年2月26日-框架篇[Mybatis篇]-Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式 ?
  • linux 下 ohmyzsh,miniconda 全局安装
  • 本地化语音识别CapsWriter结合内网穿透远程会议录音秒变文字稿
  • Java高频面试之集合-11
  • Day08 实例:3个网站实例探究算法对于密文加密的影响
  • Kafka 消费者组的重平衡
  • 深度学习优化-Gradient Checkpointing
  • ORACLE 19.8版本遭遇ORA-600 [kqrHashTableRemove: X lock].宕机的问题分析
  • CSS:不设定高度的情况,如何让flex下的两个元素的高度一致
  • 历次科技泡沫对人工智能发展的启示与规避措施
  • Python----计算机视觉处理(opencv:图片灰度化)
  • Unity屏幕适配——立项时设置
  • Python使用FastAPI结合Word2vec来向量化200维的语言向量数值
  • 缓存使用的具体场景有哪些?缓存的一致性问题如何解决?缓存使用常见问题有哪些?