鸿蒙面试题库收集(二):ArkTS&ArkUI数据存储&组件&布局

3. 数据存储

1. LocalStorage和AppStorage的区别,和对应的装饰器以及PersistentStorage ***
LocalStorage

页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。

localStorage是页面级数据存储,在页面中创建实例,组件中使用@LocalStorageLink和@LocalStorageProp装饰器修饰对应的状态变量,绑定对应的组件使用比状态属性更灵活

//应用逻辑使用LocalStorage
let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化 , 创建实例存储数据
let propA: number | undefined = storage.get('PropA') // propA == 47 ,get()获取数据
//link():如果给定的propName在LocalStorage实例中存在,则返回与LocalStorage中propName对应属性的双向绑定数据。 (双向同步)
let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47
//prop():如果给定的propName在LocalStorage中存在,则返回与LocalStorage中propName对应属性的单向绑定数据。  (单向同步)
let prop: SubscribedAbstractProperty<number> = storage.prop('PropA'); // prop.get() == 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48 //双向同步
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48 //单向同步
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
  • new LocalStorage(数据Object)创建实例并存储数据

  • set方法,设置数据

  • get方法,获取数据

  • link方法,返回一个双向同步的变量

    • 与UI逻辑中@LocalStorageLink装饰器类似
  • prop方法,返回单向同步的变量

    • 与UI逻辑中@LocalStorageProp装饰器类似
//UI使用LocalStorage
//除了应用程序逻辑使用LocalStorage,还可以借助LocalStorage相关的两个装饰器@LocalStorageProp和@LocalStorageLink,在UI组件内部获取到LocalStorage实例中存储的状态变量。
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
//这个变量一般就定义在某个@Entry组件的上方**
let storage: LocalStorage = new LocalStorage(para);

@Component
struct Child {
 // @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
 @LocalStorageLink('PropA') storageLink2: number = 1;

 build() {
   Button(`Child from LocalStorage ${this.storageLink2}`)
     // 更改将同步至LocalStorage中的'PropA'以及Parent.storageLink1
     .onClick(() => {
       this.storageLink2 += 1
     })
 }
}
// 使LocalStorage可从@Component组件访问
@Entry(storage)//storage是上边定义的变量**
@Component
struct Parent {
 // @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
 @LocalStorageLink('PropA') storageLink1: number = 1;

 build() {
   Column({ space: 15 }) {
     Button(`Parent from LocalStorage ${this.storageLink1}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already
       .onClick(() => {
         this.storageLink1 += 1
       })
     // @Component子组件自动获得对CompA LocalStorage实例的访问权限。
     Child()
   }
 }
}
//上述代码:如果将LocalStorageLink改为LocalStorageProp就由双向变为了单向
  • new LocalStorage(数据Object)创建实例并存储数据
  • 父组件:
    • @Entry(LocalStorage实例) , 将LocalStorage数据注册到页面中,使得页面内部可以使用数据
    • @LocalStorageLink ,将页面变量与数据进行双向绑定,父子组件都可以使用
    • @LocalStorageProp,将页面变量与数据进行单向绑定,父子组件都可以使用
AppStorage

AppStorage是进程级数据存储(==应用级的全局状态共享==),进程启动时自动创建了唯一实例,在各个页面组件中@StorageProp和@StorageLink装饰器修饰对应的状态变量。

//AppStorage是单例,它的所有API都是静态的
AppStorage.setOrCreate('PropA', 47);

let propA: number | undefined = AppStorage.get('PropA') // propA in AppStorage == 47
let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); // prop.get() == 47

link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49

AppStorage.get<number>('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49
  • 语法基本上与LocalStorage类似,只不过是静态方法
  • link双向,prop单向
AppStorage.setOrCreate('PropA', 47);

@Entry(storage)
@Component
struct CompA {
  @StorageLink('PropA') storageLink: number = 1;

  build() {
    Column({ space: 20 }) {
      Text(`From AppStorage ${this.storageLink}`)
        .onClick(() => {
          this.storageLink += 1
        })
    }
  }
}
  • @StorageLink换为@StorageProp,双向就变为单向的了
  • 不建议使用@StorageLink , 其实就是因为AppStorage共享范围太多,更新效率低下,也可能造成不必要的更新

补充:

localStorage和appStorage数据存取都是在主线程进行的,且api只提供了同步接口,存取数据时要注意数据的大小。

  • 关于存储时数据的大小问题
    • AppStorage没有大小限制,单条数据[k,v)],v也没有限制,但是不建议单条v大于1kb,大于1kb建议使用数据库。多条使用没有限制,会动态分配的。
    • LocalStorage底层实现是一个map,理论上没有大小限制。
PersistentStorage

PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。

PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的

从AppStorage中访问PersistentStorage初始化的属性

  1. 初始化PersistentStorage:

    PersistentStorage.persistProp('aProp', 47);
  2. 在AppStorage获取对应属性:

    AppStorage.get<number>('aProp'); // returns 47

    或在组件内部定义:

    @StorageLink('aProp') aProp: number = 48;
2.数据存储怎么存?都用过什么数据存储?用户首选项,键值型数据库,关系数据库
首选项

用户首选项(Preferences):提供了==轻量级配置数据的持久化能力==,并支持订阅数据变化的通知能力。不支持分布式同步,常用于保存==应用配置信息、用户偏好设置==等。
约束限制:

  • Key键为string类型,要求非空且长度不超过==80个字节==
  • 如果Value值为string类型,请使用UTF-8编码格式,可以为空,不为空时长度不超过==8192个字节==
  • 内存会随着存储数据量的增大而增大,所以存储的数据量应该是轻量级的,建议存储的数据==不超过一万条==,否则会在内存方面产生较大的开销

代码:

import dataPreferences from '@ohos.data.preferences';
//1. 获取preference
private preferences: dataPreferences.Preferences =  dataPreferences.getPreferencesSync(this.context, { name: 'myStore' });

//2. 保存数据
this.preferences.putSync('key', value);
//3. 持久化数据
this.preferences.flush()
//4. 获取数据
let result = this.preferences.getSync("key",16)
this.changeFontSize = Number(result)
键值型数据库

键值型数据库存储键值对形式的数据,当需要存储的数据没有复杂的关系模型,比如存储商品名称及对应价格、员工工号及今日是否已出勤等,由于数据复杂度低,更容易兼容不同数据库版本和设备类型,因此推荐使用键值型数据库持久化此类数据。

约束:

  • 设备协同数据库,针对每条记录,Key的长度≤896 Byte,Value的长度<4 MB。

  • 单版本数据库,针对每条记录,Key的长度≤1 KB,Value的长度<4 MB。

  • 每个应用程序最多支持同时打开16个键值型分布式数据库。

  • 键值型数据库事件回调方法中不允许进行阻塞操作,例如修改UI组件。

剩余内容:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-by-kv-store-0000001820999693

关系型数据库

关系型数据库基于SQLite组件,适用于存储包含==复杂关系数据==的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。

运作机制:

<img src="0.Harmony面试题.assets/image-20240521094339897.png" alt="image-20240521094339897" style="zoom: 33%;" />

约束:

  • 数据库中有4个读连接和1个写连接,线程获取到空闲读连接时,即可进行读取操作。当没有空闲读连接且有空闲写连接时,会将写连接当做读连接来使用。
  • 为保证数据的准确性,数据库同一时间只能支持一个写操作。
  • 当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除。
  • 为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。

代码:

import relationalStore from '@ohos.data.relationalStore'
import { common } from '@kit.AbilityKit'

export  class DBUtils {
  // 数据库名称
  private tableName: string = 'accountTable'
  // 建表语句
  private sqlCreate: string = 'CREATE TABLE IF NOT EXISTS accountTable(id INTEGER PRIMARY KEY AUTOINCREMENT, accountType INTEGER, ' +
    'typeText TEXT, amount INTEGER)'
  // 表字段
  private columns: string[] = ['id', 'accountType', 'typeText', 'amount']
  // 数据库核心类
  private rdbStore: relationalStore.RdbStore | null = null
  // 数据库配置
  DB_CONFIG: relationalStore.StoreConfig = {
    name: 'RdbTest.db', // 数据库文件名
    securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别
  };

  /**
   * 获取rdb
   * @param context:上下文
   * @param callback:回调函数,我们第一次获取数据时,需要在获取到rdb之后才能获取,所以有此回调
   */
  getRdbStore(context: common.UIAbilityContext, callback: Function) {
    relationalStore.getRdbStore(context, this.DB_CONFIG, (error, store) => {
      if (this.rdbStore !== null) {
        //如果已经有rdb,直接建表
        store.executeSql(this.sqlCreate)
        return
      }
      //保存rdb,下边会用
      this.rdbStore = store
      //建表
      store.executeSql(this.sqlCreate)
      console.log("test", "successed get dbStore")
      if (callback) callback()
    })
  }

  /**
   * 插入数据
   * @param data:数据对象
   * @param callback:回调函数,这里的结果是通过回调函数返回的(也可使用返回值)
   */
  insertData(data: AccountData, callback: Function) {
    //将数据对象,转换为ValuesBucket类型
    const valueBucket: relationalStore.ValuesBucket = generateBucket(data);
    // 调用insert插入数据
    this.rdbStore && this.rdbStore.insert(this.tableName, valueBucket, (err, res) => {
      if (err) {
        console.log("test,插入失败", err)
        callback(-1)
        return
      }
      console.log("test,插入成功", res)
      callback(res) //res为行号
    })

  }

  /**
   * 获取数据
   * @param callback:接收结果的回调函数
   */
  query(callback: Function) {
    //predicates是用于添加查询条件的
    let predicates = new relationalStore.RdbPredicates(this.tableName)
    // 查询所有,不需要条件
    // predicates.equalTo("字段",数据)
    this.rdbStore && this.rdbStore.query(predicates, this.columns, (error, resultSet: relationalStore.ResultSet) => {
      if(error){
        console.log("test,获取数据失败",JSON.stringify(error))
        return
      }
      let count: number = resultSet.rowCount
      console.log("test","数据库中数据数量:"+count) //没数据时返回-1或0
      if (count <= 0 || typeof count === 'string') {
        callback([])
        return
      }
      let result: AccountData[] = []
      //上来必须调用一次goToNextRow,让游标处于第一条数据,while(resultSet.goToNextRow())是最有写法
      while(resultSet.goToNextRow()) {
        let accountData:AccountData = {id:0,accountType:0,typeText:'',amount:0}
        accountData.id = resultSet.getDouble(resultSet.getColumnIndex('id'));
        accountData.typeText = resultSet.getString(resultSet.getColumnIndex('typeText'))
        accountData.accountType = resultSet.getDouble(resultSet.getColumnIndex('accountType'))
        accountData.amount = resultSet.getDouble(resultSet.getColumnIndex('amount'))
        result.push(accountData)
      }
      callback(result)
      resultSet.close()//释放数据集内容
    })
  }
}
function generateBucket(account: AccountData): relationalStore.ValuesBucket {
  let obj: relationalStore.ValuesBucket = {};
  obj.accountType = account.accountType;
  obj.typeText = account.typeText;
  obj.amount = account.amount;
  return obj;
}

export class AccountData {
  id: number = -1;
  accountType: number = 0;
  typeText: string = '';
  amount: number = 0;
}

使用代码:(部分代码,从HelloDataManager中copy的)

// 数据库工具类
  private dbUitls: DBUtils = new DBUtils()

// 界面打开时,查询数据,展示胡静
  aboutToAppear(): void {
    this.dbUitls.getRdbStore(this.context, () => {
      this.queryData()
    })
  }
  // 查询数据方法
  queryData(){
    this.dbUitls.query((result: AccountData[]) => {
      this.accountDataArray = result
      console.log("test,获取数据成功:", JSON.stringify(this.accountDataArray))
    })
  }
  // 点击确定回调
  onConfirm(insertData: AccountData) {
    console.log("test", JSON.stringify(insertData))
    // 插入数据
    this.dbUitls.insertData(insertData, (res: number) => {
      if (res > 0) {
        // AlertDialog.show({ message: "添加成功" })
        this.queryData()
      } else {
        AlertDialog.show({ message: "添加失败" })
      }
    })
  }

4. 组件&布局

1.用过flex布局吗?flex存在的问题是什么?为什么会造成二次渲染?如何优化?
  • 用过flex

    • 弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。

      容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。

  • flex会造成二次渲染

    • flex中flexgrow=1时,子组件宽度和大于flex的宽度时,页面渲染后,会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染
      flex中flexshrink=1时,子组件宽度和小于flex的宽度时,页面渲染后,也会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染

    • flexGrow:设置父容器的剩余空间分配给此属性所在组件的比例。用于分配父组件的剩余空间。

      flexShrink: 当父容器空间不足时,子元素的压缩比例。

      参考链接:https://www.seaxiang.com/blog/8bf810f30c9f4fe7a24fb55c4778ed6c

  • 优化

    • 使用Column/Row代替Flex。
    • 大小不需要变更的子组件主动设置flexShrink属性值为0。
    • 优先使用layoutWeight属性替代flexGrow属性和flexShrink属性。
    • 子组件主轴长度分配设置为最常用场景的布局结果,使子组件主轴长度总和等于Flex容器主轴长度。
  • 参考链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-flex-layout-0000001774120126#ZH-CN_TOPIC_0000001774120126__%E6%A6%82%E8%BF%B0

2. flex导致二次布局的原因,以及调研的经历
flex中flexgrow=1时,子组件宽度和大于flex的宽度时,页面渲染后,会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染
flex中flexshrink=1时,子组件宽度和小于flex的宽度时,页面渲染后,也会调整子组件宽度使之宽度和等于flex的宽度,造成二次布局渲染
3.使用了哪些组件,有没有写过自定义组件,自定义组件怎么设计的
  • Button是按钮组件,通常用于响应用户的点击操作,其类型包括胶囊按钮、圆形按钮、普通按钮。Button做为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。

    Text是文本组件,通常用于展示用户视图,如显示文章的文字

  • 写过自定义组件

  • 自定义组件怎么设计,有两种情况

    1. 首先公司内部将常用的一些效果 , 都封装了组件
      1. 比如: listView下拉刷新动画 , 进度条等等
      2. 具体自定义组件如何定义 , 参考上述链接
    2. 其次就是一个应用中 , 如果一个布局在多个页面中出现 , 我们就可以将这个布局效果封装为一个组件
      1. 比如: 页面的头部标题 , 点赞 , 收藏等
      2. 具体自定义组件如何定义 , 参考上述链接
4. 自定义组件,实际开发中封装过哪些自定义组件

例如:下拉刷新、自定义弹窗、自定义LoadingProgress、统一标题栏的封装等...

自定义组件具有以下特点:

  • 可组合:允许组合使用系统组件、及其属性和方法。
  • 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
  • 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。

自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。

struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。

5.项目中用到arkTs哪些技术

arkTs的技术?
装饰器
渲染控制:

6.flex-shrink的使用

答: 设置父容器压缩尺寸分配给此属性所在组件的比例, 设置为==0表示不压缩==,
父容器为Row、Column时,默认值:0。
父容器为flex时,默认值:1。
在子组件的总宽度大于父容器的宽度时,子组件中有设置flex-shrink , 并且值大于0时,父容器会把剩余宽度按照比例压缩子组件。Flex布局-通用属性-组件通用信息-组件参考(基于ArkTS的声明式开发范式)-ArkTS API参考-HarmonyOS应用开发

关于flexShrink如何计算,参考:https://www.jianshu.com/p/f64eb6613d35
自己总结公式:
溢出值:所有子元素宽度相加 - 父元素宽度
总权重:子元素1的flexShrink*子元素1宽度 + 子元素2的flexShrink*子元素2宽度 +...
子元素压缩值:溢出值 * 子元素的权重 = 溢出值 * 子元素1的flexShrink*子元素1宽度/总权重

7.webview组件如何使用,ets文件如何与h5通讯 ***

总结:两种方式:
方式一:

runJavaScript() arkts-》H5 
javaScriptProxy() 和 registerjavaScriptProxy() H5-》arkts

方式二:

createWebMessagePorts postMessage onMessageEvent 数据通道

这里的方案是,相互调用函数来通信,12题是通过消息端口建立数据通道

  1. @ohos.web.webview提供web控制能力,web组件提供网页显示的能力

  2. webview组件如何使用?

    1. 添加网络权限: ohos.permission.INTERNET
    2. 加载网页
      // xxx.ets
      import web_webview from '@ohos.web.webview'
      
      @Entry
      @Component
      struct WebComponent {
      controller: web_webview.WebviewController = new web_webview.WebviewController()
      build() {
        Column() {
          //加载在线网页
          Web({ src: 'www.example.com', controller: this.controller })
          //加载本地网页
          // 通过$rawfile加载本地资源文件。
          Web({ src: $rawfile("index.html"), controller: this.controller })
          // 通过resource协议加载本地资源文件。
          Web({ src: "resource://rawfile/index.html", controller: this.controller })
        }
      }
      }
  3. ets文件如何与h5通讯?

    1. ets调用h5网页方法

      1. h5定义方法
        <!-- index.html -->
        <!DOCTYPE html>
        <html>
        <meta charset="utf-8">
        <body>
         
        </body>
        <script type="text/javascript">
          function htmlFn() {
              console.log('run javascript test')
              return "This value is from index.html"
          }
          </script>
        </html>
      2. ets调用
        Button('runJavaScript')
                .onClick(() => {
                  console.log("run-onclick")
                  //点击按钮,运行js方法
                  this.controller.runJavaScript('htmlFn()', (err, val) => {
                    if (err) {
                      console.log('run javascript err ' + JSON.stringify(err))
                      return
                    }
                    if (val) {
                      this.message = val
                    }
                    console.log('run javascript success ' + val);
                  })
                })
                .width('30%')
                .height('30')
    2. h5调用ets方法

      1. ets定义方法

        testObj = {
         
            test:() => {
              this.message = '调用了当前方法'
              console.log('javascript run test 调用了当前方法')
              return 'ArkUI Web Component'
            },
            toString:() => {
              console.log('Web Component toString')
            }
          }
      2. 注入到h5

        Button('Register JavaScript To Window')
                .onClick(() => {
                  AlertDialog.show({message:'注册方法成功'})
                  try {
                      //注册方法到H5的控制器
                    //参数1:传入调用方法的对象
                    //参数2:H5在使用该对象的名字
                    //参数3:方法列表(数组)
                    this.controller.registerJavaScriptProxy(this.testObj, "testObjName", ["test", "toString"]);
                  } catch (error) {
                    console.error(`ErrorCode: ${error.code},  Message: ${error.message}`);
                  }
                })
        //registerJavaScriptProxy注册的方法,必须刷新才能使用
         Button('refresh')
                .onClick(() => {
                  AlertDialog.show({message:'刷新'})
                  try {
                    this.controller.refresh();
                  } catch (error) {
                    console.error(`ErrorCode: ${error.code},  Message: ${error.message}`);
                  }
                })
        
        //或者不适用registerJavaScriptProxy方法来注册,直接使用Web的方法javaScriptProxy
           Web({ src: $rawfile("second.html"), controller: this.controller })
                // 将对象注入到web端
                .javaScriptProxy({
                  object: this.testObj,
                  name: "testObjName",
                  methodList: ["test", "toString"],
                  controller: this.controller
                })
        //有什么区别呢?目前官网文档中的例子是,如果ets的方法test返回的是复杂类型的数据,比如数组,那么使用的是registerJavaScriptProxy
        //但是经过测试,本地模拟器和远程模拟器,都无法传递,可能是模拟器的问题
      3. h5调用

        <!-- index.html -->
        <!DOCTYPE html>
        <html>
        <meta charset="utf-8">
        <body>
        <button style="width:200px;height:200px" type="button" onclick="htmlTest()">Click Me!</button>
        <p id="demo"></p>
        </body>
        <script type="text/javascript">
            function htmlTest() {
              let str=testObjName.test();
              document.getElementById("demo").innerHTML=str;
              console.log('testObj.test result:'+ str)
            }
        </script>
        </html>
    3. 参考链接: https://blog.csdn.net/Lu_Ca/article/details/135285413或官网https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-in-page-app-function-invoking-0000001774279950

    4. 代码在InterviewQuestion项目中的WebComponent2

注意:

  • 应用端通过消息端口(WebMessagePort):发送消息使用postMessageEvent,接收消息使用onMessageEvent
  • html端通过消息端口:发送消息使用postMessage,接收消息监听事件onmessage (都少了event)

问题1: 模拟器中web加载网页后,网页的按钮无法点击。
在模拟器中,是无法测试html端---应用端的数据传递。

  • 本地的模拟器现在不支持webview
  • 可以使用远程模拟器,或远程真机

问题2: 在将端口0发送到html端时,使用的方法是postMessage,而真正发送数据是通过postMessageEvent,区别在哪?

  • postMessage就是用于发送消息端口的
  • postMessageEvent就是用于发送消息的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

媛媛要加油呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值