分类 其它 下的文章

简介

PasteForm是贴代码推出的 “新一代CRUD” ,基于ABPvNext,目的是通过对Dto的特性的标注,从而实现管理端的统一UI,借助于配套的PasteBuilder代码生成器,你可以快速的为自己的项目构建后台管理端!目前管理端只有Html+js版本的,后续将支持小程序,Vue等

案例源码

案例源码在

https://gitee.com/pastecode/paste-template

不定期升级

AllInDto!

通过引入PasteForm,一个项目哪怕100个数据表,一般的管理页面也才不到10个,除非有非常多的特殊功能,否则都能用PasteForm中的表格和表单来实现!

image

MarkDown

在开发管理端过程中,有时候也需要使用用到Markdown,之前已经接入了Richtext,本次升级也一并把这个带进去了!
首先你需要从案例项目的PasteTemplate的前端模块
PasteTemplate.HttpApi.Host/wwwroot/page/lib/editor.md/
注意这个里面就是Markdown用到的组件,用的是三方的!

特性信息

如果是字符串,没有设置maxlength,默认就会变更成richtext,也可以手动强制配置,当前特性适用于richtext和markdown

字段 类型 示例 说明
args1 字符 500 配置高度,默认为500
args2 字符 txt 表示另外一个值存储在哪个字段,所以另外一个字段一般设置隐藏
args3 字符 /api/app/Upload/Image 图片上传的地址

以上信息同样适用于richtext

案例UI

image

以上是我用双屏截图的,其实是一个完整的页面,注意看页面的表单中包含了text,textarea,richtext,markdown
那么他对应的dto是如何写的?

Dto对应代码

    /// <summary>
    /// 
    /// </summary>
    public class StringDto
    {
        ///<summary>
        ///姓名 模拟短文本输入
        ///</summary>
        [MaxLength(32)]
        [Required]
        public string Name { get; set; }

        ///<summary>
        ///文本区域 模拟文本区域的输入
        ///</summary>
        [MaxLength(128)]
        public string Desc { get; set; }

        ///<summary>
        ///文本区域 长度大于128则自动为textarea
        ///</summary>
        [MaxLength(256)]
        public string Text { get; set; }

        /////<summary>
        /////文本区域 可以手动指定为textarea,同理你也可以指定为html,text
        /////</summary>
        //[MaxLength(128)]
        //[ColumnDataType("textarea")]
        //public string Mark { get; set; }

        ///<summary>
        ///富文本 模拟富文本,前端HTML的是使用wangEditv5,默认不配置maxlength的就是html
        ///</summary>
        public string Blog { get; set; }

        /////<summary>
        /////MarkDown1 
        /////</summary>
        //[ColumnDataType("markdown")]
        //public string Mark1 { get; set; }

        ///<summary>
        ///MarkDown2 有默认值的
        ///</summary>
        [ColumnDataType("markdown")]
        public string Mark2 { get; set; } = "## 今日成果";

    }

由上面代码可知,只要在对应的字段上配置
[ColumnDataType("markdown")]即可
或者你也可以配置特性为[PasteMarkDown]
他们两个是等效的,可以说ColumnDataTypeAttribute是所有贴代码框架特性的基础属性
像PasteClass,PasteHidden,PasteButton等最终都是为了转化成ColumnDataTypeAttribute

提交信息

上面的UI中,我们是随便填写点东西后,提交,看到的提交信息如下

image

暂时先忽略mark2mdeditor-html-code和mark2mdeditor-markdown-doc字段,这个是markdown框架里面带了name被parseform来了,后续再考虑去掉他!
从上面的提交可以看到mark2是有内容的!

符合预期!

注意

由于markdown和richtext的特殊性,关于字段长度的设置需要按照实际来填写!
看特性信息args2,这个字段如何配置了,则需要对这个字段的特性标注为hidden
这样在UI上args2的字段是不显示的,而提交数据给后台,则可以接收到!

我们下期将介绍select和reload的巧妙结合案例... .. .

前言

最近阅读
Aravis
源码,其中大量运用了GObject,于是打算学习一下。

此系列笔记仅主要面向初学者,不会很深入探讨源码的细节,专注于介绍GObject的基本用法。

此系列笔记参考
GObject Tutorial for beginners

本文可在
个人博客
中阅读,体验更加

套个盾:文中定义的名词只是为了更好地理解GObject,不具备权威性。

类和实例

在GObject中,每个可实例化类类型都与两个结构体相关联:一个是类结构体,一个是实例结构体。

  • 类结构体会被注册到类型系统中(具体注册方式在
    下一节
    讨论),在
    g_object_new
    首次调用时,类型系统会检查相应的类结构体是否已经被初始化为一个类变量,没有则创建并初始化。此后所有该类的实例变量都将共享这个已初始化的类变量。每个类变量只会被创建一次。
  • 每次调用
    g_object_new
    时都会创建实例变量。

在GObject系统中,类结构体和实例结构体都会被实例化,在内存中占有特定的空间。为了便于描述,我们将分配给类结构体的实例称为“类变量”,而分配给实例结构体的实例称为“实例变量”。

GObject实例的结构体定义如下

//file: gobject.h
typedef struct _GObject  GObject;
struct  _GObject
{
  GTypeInstance  g_type_instance;
  
  /*< private >*/
  guint          ref_count;  /* (atomic) */
  GData         *qdata;
};

GObject类的结构体定义如下(我们可以先不用了解结构的细节):

//file: gobject.h
typedef struct _GObjectClass             GObjectClass;
struct  _GObjectClass
{
  GTypeClass   g_type_class;

  /*< private >*/
  GSList      *construct_properties;

  /*< public >*/
  /* seldom overridden */
  GObject*   (*constructor)     (GType                  type,
                                 guint                  n_construct_properties,
                                 GObjectConstructParam *construct_properties);
  /* overridable methods */
  void       (*set_property)		(GObject        *object,
                                         guint           property_id,
                                         const GValue   *value,
                                         GParamSpec     *pspec);
  void       (*get_property)		(GObject        *object,
                                         guint           property_id,
                                         GValue         *value,
                                         GParamSpec     *pspec);
  void       (*dispose)			(GObject        *object);
  void       (*finalize)		(GObject        *object);
  /* seldom overridden */
  void       (*dispatch_properties_changed) (GObject      *object,
					     guint	   n_pspecs,
					     GParamSpec  **pspecs);
  /* signals */
  void	     (*notify)			(GObject	*object,
					 GParamSpec	*pspec);

  /* called when done constructing */
  void	     (*constructed)		(GObject	*object);

  /*< private >*/
  gsize		flags;

  gsize         n_construct_properties;

  gpointer pspecs;
  gsize n_pspecs;

  /* padding */
  gpointer	pdummy[3];
};

下面使用一个简单示例,来演示GObject的类和实例的使用

//file: example01.c
#include <glib-object.h>

int main (int argc, char **argv) 
{

    GObject* instance1,* instance2;     //指向实例的指针
    GObjectClass* class1,* class2;      //指向类的指针
   
    instance1 = g_object_new (G_TYPE_OBJECT, NULL);
    instance2 = g_object_new (G_TYPE_OBJECT, NULL);
    g_print ("The address of instance1 is %p\n", instance1);
    g_print ("The address of instance2 is %p\n", instance2);
 
    class1 = G_OBJECT_GET_CLASS (instance1);
    class2 = G_OBJECT_GET_CLASS (instance2);
    g_print ("The address of the class of instance1 is %p\n", class1);
    g_print ("The address of the class of instance2 is %p\n", class2);
 
    g_object_unref (instance1);
    g_object_unref (instance2);

    return 0;
}

其中:

  • g_object_new
    函数创建实例变量并返回指向它的指针。在实例变量第一次被创建之前,它对应的类变量也会被创建并初始化。
  • 参数
    G_TYPE_OBJECT
    是GObject基类的类型标识符,这是GObject类型系统的核心,所有其他GObject类型都从这个基类型派生。

  • G_OBJECT_GET_CLASS
    返回指向参数所属类变量的指针
  • g_object_unref
    会销毁实例变量并释放内存。

输出:

The address of instance1 is 0x55d3ddc05600
The address of instance2 is 0x55d3ddc05620
The address of the class of instance1 is 0x55d3ddc05370
The address of the class of instance2 is 0x55d3ddc05370

可以发现,两个实例变量的地址不同,但两个实例变量对应的类变量的地址相同,因为两个实例变量共享一个类变量

引用计数

引用计数机制的概念在此不做介绍

在GObject中,GObject实例具有引用计数机制:

//file: example02.c
#include <glib-object.h>
 
static void show_ref_count (GObject* instance) 
{
    if (G_IS_OBJECT (instance))
        /* Users should not use ref_count member in their program. */
        /* This is only for demonstration. */
        g_print ("Reference count is %d.\n", instance->ref_count);
    else
        g_print ("Instance is not GObject.\n");
}
 
int main (int argc, char **argv) 
{
    GObject* instance;

    instance = g_object_new (G_TYPE_OBJECT, NULL);
    g_print ("Call g_object_new.\n");
    show_ref_count (instance);
    g_object_ref (instance);
    g_print ("Call g_object_ref.\n");
    show_ref_count (instance);
    g_object_unref (instance);
    g_print ("Call g_object_unref.\n");
    show_ref_count (instance);
    g_object_unref (instance);
    g_print ("Call g_object_unref.\n");
    g_print ("Now the reference count is zero and the instance is destroyed.\n");
    g_print ("The instance memories are possibly returned to the system.\n");
    g_print ("Therefore, the access to the same address may cause a segmentation error.\n");

    return 0;
}

其中:

  • g_object_new
    创建一个实例变量,然后将变量的引用计数置为1
  • g_object_ref
    将其引用计数加1
  • g_object_unref
    将引用计数减1,如果此时引用计数为0,则析构变量。

输出:

Call g_object_new.
Reference count is 1.
Call g_object_ref.
Reference count is 2.
Call g_object_unref.
Reference count is 1.
Call g_object_unref.
Now the reference count is zero and the instance is destroyed.
The instance memories are possibly returned to the system.
Therefore, the access to the same address may cause a segmentation error.

初始化和析构过程

GObject初始化和销毁的实际过程比较复杂。以下是简单的描述,不做详细说明.

初始化

1.用类型系统注册GObject类型。这是在调用main函数之前的GLib的初始化过程中完成的。(如果编译器是gcc,则
__attribute__ ((constructor))
用于限定初始化函数。)
2.为GObjectClass和GObject结构分配内存
3.初始化GObjectClass结构内存。这个内存将是GObject的类变量。
4.初始化GObject结构内存。这个内存将是GObject的实例变量。

上述初始化过程在第一次调用
g_object_new
函数时执行。在第二次及后续调用
g_object_new
时,它只执行两个过程:①为GObject结构分配内存②初始化内存。

析构

1.销毁GObject实例。释放实例的内存

GObject变量类型是静态类型。静态类型永远不会破坏它的类。因此,即使被销毁的实例变量是最后一个,类变量仍然存在,直到程序终止。

参考文章

1.
GObject Tutorial for beginners

推荐

下一篇:
GObject学习笔记(二)类型创建与注册

最近微信出了linux版,用vmware装linux不过瘾,把一台闲置的笔记本装上了Manjaro KDE Plasma,经过一段时间的发展,Linux桌面可用性大大提高。
Kindle->Kindle Mate->Anki这条路在linux下
我用
Kindle ->
KindleVocab
->Anki这么代替了之后,
其他软件都能凑合用,加之用了电信的天翼云电脑后觉得又补全了几乎所有的缺憾

感谢信创,感谢国家,国内大公司出的软件都有信创包了

但是

天翼云电脑只有deb包(
https://desk.ctyun.cn/html/download/)

aur上的也安装不成功

我对yay命令不熟,只有用笨办法解决
首先是下载deb包

装了dpkg
安装失败

看失败信息,把这些包查了下chatgpt后都补上

然后用dpkg的强制安装。虽然报错,但是功能基本正常

【引言】(完整代码在最后面)

本文将通过一个简单的计数器应用案例,介绍如何利用鸿蒙NEXT的特性开发高效、美观的应用程序。我们将涵盖计数器的基本功能实现、用户界面设计、数据持久化及动画效果的添加。

【环境准备】

电脑系统:windows 10

开发工具:DevEco Studio 5.0.1 Beta3 Build Version: 5.0.5.200

工程版本:API 13

真机:Mate60 Pro

语言:ArkTS、ArkUI

【项目概述】

本项目旨在创建一个多计数器应用,用户可以自由地添加、编辑、重置和删除计数器。每个计数器具有独立的名称、当前值、增加步长和减少步长。应用还包括总计数的显示,以便用户快速了解所有计数器的总和。

【功能实现】

1、计数器模型

首先,我们定义了一个CounterItem类来表示单个计数器,其中包含了计数器的基本属性和行为。

@ObservedV2
class CounterItem {
  id: number = ++Index.counterId;
  @Trace name: string;
  @Trace count: number = 0;
  @Trace scale: ScaleOptions = { x: 1, y: 1 };
  upStep: number = 1;
  downStep: number = 1;

  constructor(name: string) {
    this.name = name;
  }
}

2、应用入口与状态管理

应用的主入口组件Index负责管理计数器列表、总计数、以及UI的状态。这里使用了@State和@Watch装饰器来监控状态的变化。

@Entry
@Component
struct Index {
  static counterStorageKey: string = "counterStorageKey";
  static counterId: number = 0;

  @State listSpacing: number = 20;
  @State listItemHeight: number = 120;
  @State baseFontSize: number = 60;
  @State @Watch('updateTotalCount') counters: CounterItem[] = [];
  @State totalCount: number = 0;
  @State isSheetVisible: boolean = false;
  @State selectedIndex: number = 0;

  // ...其他方法
}

3、数据持久化

为了保证数据在应用重启后仍然可用,我们使用了preferences模块来同步地读取和写入数据。

saveDataToLocal() {
  const saveData: object[] = this.counters.map(counter => ({
    count: counter.count,
    name: counter.name,
    upStep: counter.upStep,
    downStep: counter.downStep,
  }));

  this.dataPreferences?.putSync(Index.counterStorageKey, JSON.stringify(saveData));
  this.dataPreferences?.flush();
}

4、用户界面

用户界面的设计采用了现代简洁的风格,主要由顶部的总计数显示区、中间的计数器列表区和底部的操作按钮组成。列表项支持左右滑动以显示重置和删除按钮。

@Builder
itemStart(index: number) {
  Row() {
    Text('重置').fontColor(Color.White).fontSize('40lpx').textAlign(TextAlign.Center).width('180lpx');
  }
  .height('100%')
  .backgroundColor(Color.Orange)
  .justifyContent(FlexAlign.SpaceEvenly)
  .borderRadius({ topLeft: 10, bottomLeft: 10 })
  .onClick(() => {
    this.counters[index].count = 0;
    this.updateTotalCount();
    this.listScroller.closeAllSwipeActions();
  });
}

5、动画效果

当用户添加新的计数器时,通过动画效果让新计数器逐渐放大至正常尺寸,提升用户体验。

this.counters.unshift(new CounterItem(`新计数项${Index.counterId}`));
this.listScroller.scrollTo({ xOffset: 0, yOffset: 0 });
this.counters[0].scale = { x: 0.8, y: 0.8 };

animateTo({
  duration: 1000,
  curve: curves.springCurve(0, 10, 80, 10),
  iterations: 1,
  onFinish: () => {}
}, () => {
  this.counters[0].scale = { x: 1, y: 1 };
});

【总结】

通过上述步骤,我们成功地构建了一个具备基本功能的计数器应用。在这个过程中,我们不仅学习了如何使用鸿蒙NEXT提供的各种API,还掌握了如何结合动画、数据持久化等技术点来优化用户体验。希望本文能为你的鸿蒙开发之旅提供一些帮助和灵感!

【完整代码】

import { curves, promptAction } from '@kit.ArkUI' // 导入动画曲线和提示操作
import { preferences } from '@kit.ArkData' // 导入偏好设置模块

@ObservedV2
  // 观察者装饰器,监控状态变化
class CounterItem {
  id: number = ++Index.counterId // 计数器ID,自动递增
  @Trace name: string // 计数器名称
  @Trace count: number = 0 // 计数器当前值,初始为0
  @Trace scale: ScaleOptions = { x: 1, y: 1 } // 计数器缩放比例,初始为1
  upStep: number = 1 // 增加步长,初始为1
  downStep: number = 1 // 减少步长,初始为1

  constructor(name: string) { // 构造函数,初始化计数器名称
    this.name = name
  }
}

@Entry
  // 入口组件装饰器
@Component
  // 组件装饰器
struct Index {
  static counterStorageKey: string = "counterStorageKey" // 存储计数器数据的键
  static counterId: number = 0 // 静态计数器ID
  @State listSpacing: number = 20 // 列表项间距
  @State listItemHeight: number = 120 // 列表项高度
  @State baseFontSize: number = 60 // 基础字体大小
  @State @Watch('updateTotalCount') counters: CounterItem[] = [] // 计数器数组,监控总计数更新
  @State totalCount: number = 0 // 总计数
  @State isSheetVisible: boolean = false // 控制底部弹出表单的可见性
  @State selectedIndex: number = 0 // 当前选中的计数器索引
  listScroller: ListScroller = new ListScroller() // 列表滚动器实例
  dataPreferences: preferences.Preferences | undefined = undefined // 偏好设置实例

  updateTotalCount() { // 更新总计数的方法
    let total = 0; // 初始化总计数
    for (let i = 0; i < this.counters.length; i++) { // 遍历计数器数组
      total += this.counters[i].count // 累加每个计数器的count值
    }
    this.totalCount = total // 更新总计数
    this.saveDataToLocal() // 保存数据到本地
  }

  saveDataToLocal() { // 保存计数器数据到本地的方法
    const saveData: object[] = [] // 初始化保存数据的数组
    for (let i = 0; i < this.counters.length; i++) { // 遍历计数器数组
      let counter: CounterItem = this.counters[i] // 获取当前计数器
      saveData.push(Object({
        // 将计数器数据添加到保存数组
        count: counter.count,
        name: counter.name,
        upStep: counter.upStep,
        downStep: counter.downStep,
      }))
    }
    this.dataPreferences?.putSync(Index.counterStorageKey, JSON.stringify(saveData)) // 将数据保存到偏好设置
    this.dataPreferences?.flush() // 刷新偏好设置
  }

  @Builder
  // 构建器装饰器
  itemStart(index: number) { // 列表项左侧的重置按钮
    Row() {
      Text('重置').fontColor(Color.White).fontSize('40lpx')// 显示“重置”文本
        .textAlign(TextAlign.Center)// 文本居中
        .width('180lpx') // 设置宽度
    }
    .height('100%') // 设置高度
    .backgroundColor(Color.Orange) // 设置背景颜色
    .justifyContent(FlexAlign.SpaceEvenly) // 设置内容均匀分布
    .borderRadius({ topLeft: 10, bottomLeft: 10 }) // 设置圆角
    .onClick(() => { // 点击事件
      this.counters[index].count = 0 // 重置计数器的count为0
      this.updateTotalCount() // 更新总计数
      this.listScroller.closeAllSwipeActions() // 关闭所有滑动操作
    })
  }

  @Builder
  // 构建器装饰器
  itemEnd(index: number) { // 列表项右侧的删除按钮
    Row() {
      Text('删除').fontColor(Color.White).fontSize('40lpx')// 显示“删除”文本
        .textAlign(TextAlign.Center)// 文本居中
        .width('180lpx') // 设置宽度
    }
    .height('100%') // 设置高度
    .backgroundColor(Color.Red) // 设置背景颜色
    .justifyContent(FlexAlign.SpaceEvenly) // 设置内容均匀分布
    .borderRadius({ topRight: 10, bottomRight: 10 }) // 设置圆角
    .onClick(() => { // 点击事件
      this.counters.splice(index, 1) // 从数组中删除计数器
      this.listScroller.closeAllSwipeActions() // 关闭所有滑动操作
      promptAction.showToast({
        // 显示删除成功的提示
        message: '删除成功',
        duration: 2000,
        bottom: '400lpx'
      });
    })
  }

  aboutToAppear(): void { // 组件即将出现时调用
    const options: preferences.Options = { name: Index.counterStorageKey }; // 获取偏好设置选项
    this.dataPreferences = preferences.getPreferencesSync(getContext(), options); // 同步获取偏好设置
    const savedData: string = this.dataPreferences.getSync(Index.counterStorageKey, "[]") as string // 获取保存的数据
    const parsedData: Array<CounterItem> = JSON.parse(savedData) as Array<CounterItem> // 解析数据
    console.info(`parsedData:${JSON.stringify(parsedData)}`) // 打印解析后的数据
    for (const item of parsedData) { // 遍历解析后的数据
      const newItem = new CounterItem(item.name) // 创建新的计数器实例
      newItem.count = item.count // 设置计数器的count
      newItem.upStep = item.upStep // 设置计数器的upStep
      newItem.downStep = item.downStep // 设置计数器的downStep
      this.counters.push(newItem) // 将新计数器添加到数组
    }
    this.updateTotalCount() // 更新总计数
  }

  build() { // 构建组件的UI
    Column() {
      Text('计数器')// 显示标题
        .width('100%')// 设置宽度
        .height('88lpx')// 设置高度
        .fontSize('38lpx')// 设置字体大小
        .backgroundColor(Color.White)// 设置背景颜色
        .textAlign(TextAlign.Center) // 文本居中
      Column() {
        List({ space: this.listSpacing, scroller: this.listScroller }) { // 创建列表
          ForEach(this.counters, (counter: CounterItem, index: number) => { // 遍历计数器数组
            ListItem() { // 列表项
              Row() { // 行布局
                Stack() { // 堆叠布局
                  Rect().fill("#65DACC").width(`${this.baseFontSize / 2}lpx`).height('4lpx') // 上方横条
                  Circle()// 圆形按钮
                    .width(`${this.baseFontSize}lpx`)
                    .height(`${this.baseFontSize}lpx`)
                    .fillOpacity(0)// 透明填充
                    .borderWidth('4lpx')// 边框宽度
                    .borderRadius('50%')// 圆角
                    .borderColor("#65DACC") // 边框颜色
                }
                .width(`${this.baseFontSize * 2}lpx`) // 设置宽度
                .height(`100%`) // 设置高度
                .clickEffect({ scale: 0.6, level: ClickEffectLevel.LIGHT }) // 点击效果
                .onClick(() => { // 点击事件
                  counter.count -= counter.downStep // 减少计数器的count
                  this.updateTotalCount() // 更新总计数
                })

                Stack() { // 堆叠布局
                  Text(counter.name)// 显示计数器名称
                    .fontSize(`${this.baseFontSize / 2}lpx`)// 设置字体大小
                    .fontColor(Color.Gray)// 设置字体颜色
                    .margin({ bottom: `${this.baseFontSize * 2}lpx` }) // 设置底部边距
                  Text(`${counter.count}`)// 显示计数器当前值
                    .fontColor(Color.Black)// 设置字体颜色
                    .fontSize(`${this.baseFontSize}lpx`) // 设置字体大小
                }.height('100%') // 设置高度
                Stack() { // 堆叠布局
                  Rect().fill("#65DACC").width(`${this.baseFontSize / 2}lpx`).height('4lpx') // 下方横条
                  Rect()
                    .fill("#65DACC")
                    .width(`${this.baseFontSize / 2}lpx`)
                    .height('4lpx')
                    .rotate({ angle: 90 }) // 垂直横条
                  Circle()// 圆形按钮
                    .width(`${this.baseFontSize}lpx`)// 设置宽度
                    .height(`${this.baseFontSize}lpx`)// 设置高度
                    .fillOpacity(0)// 透明填充
                    .borderWidth('4lpx')// 边框宽度
                    .borderRadius('50%')// 圆角
                    .borderColor("#65DACC") // 边框颜色
                }
                .width(`${this.baseFontSize * 2}lpx`) // 设置堆叠布局宽度
                .height(`100%`) // 设置堆叠布局高度
                .clickEffect({ scale: 0.6, level: ClickEffectLevel.LIGHT }) // 点击效果
                .onClick(() => { // 点击事件
                  counter.count += counter.upStep // 增加计数器的count
                  this.updateTotalCount() // 更新总计数
                })
              }
              .width('100%') // 设置列表项宽度
              .backgroundColor(Color.White) // 设置背景颜色
              .justifyContent(FlexAlign.SpaceBetween) // 设置内容两端对齐
              .padding({ left: '30lpx', right: '30lpx' }) // 设置左右内边距
            }
            .height(this.listItemHeight) // 设置列表项高度
            .width('100%') // 设置列表项宽度
            .margin({
              // 设置列表项的外边距
              top: index == 0 ? this.listSpacing : 0, // 如果是第一个项,设置上边距
              bottom: index == this.counters.length - 1 ? this.listSpacing : 0 // 如果是最后一个项,设置下边距
            })
            .borderRadius(10) // 设置圆角
            .clip(true) // 裁剪超出部分
            .swipeAction({ start: this.itemStart(index), end: this.itemEnd(index) }) // 设置滑动操作
            .scale(counter.scale) // 设置计数器缩放比例
            .onClick(() => { // 点击事件
              this.selectedIndex = index // 设置当前选中的计数器索引
              this.isSheetVisible = true // 显示底部弹出表单
            })

          }, (counter: CounterItem) => counter.id.toString())// 使用计数器ID作为唯一键
            .onMove((from: number, to: number) => { // 列表项移动事件
              const tmp = this.counters.splice(from, 1); // 从原位置移除计数器
              this.counters.splice(to, 0, tmp[0]) // 插入到新位置
            })

        }
        .scrollBar(BarState.Off) // 隐藏滚动条
        .width('648lpx') // 设置列表宽度
        .height('100%') // 设置列表高度
      }
      .width('100%') // 设置列宽度
      .layoutWeight(1) // 设置布局权重

      Row() { // 底部合计行
        Column() { // 列布局
          Text('合计').fontSize('26lpx').fontColor(Color.Gray) // 显示“合计”文本
          Text(`${this.totalCount}`).fontSize('38lpx').fontColor(Color.Black) // 显示总计数
        }.margin({ left: '50lpx' }) // 设置左边距
        .justifyContent(FlexAlign.Start) // 设置内容左对齐
        .alignItems(HorizontalAlign.Start) // 设置项目左对齐
        .width('300lpx') // 设置列宽度

        Row() { // 添加按钮行
          Text('添加').fontColor(Color.White).fontSize('28lpx') // 显示“添加”文本
        }
        .onClick(() => { // 点击事件
          this.counters.unshift(new CounterItem(`新计数项${Index.counterId}`)) // 添加新计数器
          this.listScroller.scrollTo({ xOffset: 0, yOffset: 0 }) // 滚动到顶部
          this.counters[0].scale = { x: 0.8, y: 0.8 }; // 设置新计数器缩放
          animateTo({
            // 动画效果
            duration: 1000, // 动画持续时间
            curve: curves.springCurve(0, 10, 80, 10), // 动画曲线
            iterations: 1, // 动画迭代次数
            onFinish: () => { // 动画完成后的回调
            }
          }, () => {
            this.counters[0].scale = { x: 1, y: 1 }; // 恢复缩放
          })
        })

        .width('316lpx') // 设置按钮宽度
        .height('88lpx') // 设置按钮高度
        .backgroundColor("#65DACC") // 设置按钮背景颜色
        .borderRadius(10) // 设置按钮圆角
        .justifyContent(FlexAlign.Center) // 设置内容居中
      }.width('100%').height('192lpx').backgroundColor(Color.White) // 设置行宽度和高度

    }
    .backgroundColor("#f2f2f7") // 设置背景颜色
    .width('100%') // 设置宽度
    .height('100%') // 设置高度
    .bindSheet(this.isSheetVisible, this.mySheet(), {
      // 绑定底部弹出表单
      height: 300, // 设置表单高度
      dragBar: false, // 禁用拖动条
      onDisappear: () => { // 表单消失时的回调
        this.isSheetVisible = false // 隐藏表单
      }
    })
  }

  @Builder
  // 构建器装饰器
  mySheet() { // 创建底部弹出表单
    Column({ space: 20 }) { // 列布局,设置间距
      Row() { // 行布局
        Text('计数标题:') // 显示“计数标题”文本
        TextInput({ text: this.counters[this.selectedIndex].name }).width('300lpx').onChange((value) => { // 输入框,绑定计数器名称
          this.counters[this.selectedIndex].name = value // 更新计数器名称
        })

      }

      Row() { // 行布局
        Text('增加步长:') // 显示“增加步长”文本
        TextInput({ text: `${this.counters[this.selectedIndex].upStep}` })// 输入框,绑定增加步长
          .width('300lpx')// 设置输入框宽度
          .type(InputType.Number)// 设置输入框类型为数字
          .onChange((value) => { // 输入框变化事件
            this.counters[this.selectedIndex].upStep = parseInt(value) // 更新增加步长
            this.updateTotalCount() // 更新总计数
          })

      }

      Row() { // 行布局
        Text('减少步长:') // 显示“减少步长”文本
        TextInput({ text: `${this.counters[this.selectedIndex].downStep}` })// 输入框,绑定减少步长
          .width('300lpx')// 设置输入框宽度
          .type(InputType.Number)// 设置输入框类型为数字
          .onChange((value) => { // 输入框变化事件
            this.counters[this.selectedIndex].downStep = parseInt(value) // 更新减少步长
            this.updateTotalCount() // 更新总计数
          })

      }
    }
    .justifyContent(FlexAlign.Start) // 设置内容左对齐
    .padding(40) // 设置内边距
    .width('100%') // 设置宽度
    .height('100%') // 设置高度
    .backgroundColor(Color.White) // 设置背景颜色
  }
}

一、背景
数据的跨时钟域处理是FPGA开发过程中的常见问题,存在两种情况

    1. 慢时钟向快时钟同步:只需在快时钟域打两拍即可。其RTL如下:

    打拍同步的原理:大家在初学FPGA时,经常听过FPGA中对信号打拍可以有效得避免亚稳态,而且一般要打两拍,其数学本质是如果打一拍发生错误得概率是1/1000,那么打两拍发生错误得概率就是1/1000000,这从统计意义上已经无限接近0。
    从电路本身的角度上讲,如果一个信号的更新频率与当前时钟域内的信号更新频率不同步,那么其和当前时钟域内的信号做任何运算,都有可能导致结果出现毛刺或者不满足下级寄存器的建立或保持时间。因此,为了确保新引人的这个非本时钟域的信号不会对本时钟域造成毁灭性打击,必须对其进行同步化处理,而方法就是使用当前时钟地域的时钟对其进行采样。而进行两次打拍是让异步信号打两拍可以让异步过来的电平信号达到一个比较“健壮”的电平区间,二避免了由于建立时间、多级扇出导致的逻辑电平不稳定的情况。以下摘自《FPGA之道》:

“为什么要采样两次呢?
采样一次,已经完成了非本时钟域信号的同步操作,那么为什么还要采用两级采样法呢?虽然从逻辑上来看,两级采样法就跟移位寄存器一样,并不能改变信号的逻辑值,而且还会增加信号传递进来的延迟,但是这绝对不是画蛇添足,而是非常必要的,原因如下:请大家思考一下,进行第一级采样的那个寄存器,其建立和保持时间是不否能够得到满足?显然在有些情况下,其建立和保持时间是无法得到满足的,因为它和当前时钟域时钟信号的变化并不同步,因此总会碰到问题。例如,当asynsignal从0变化到1时,如果在过这前后时间内,clk共经历了3个上升沿,那么unsafesignal的输出可能是001,也可能是011,其中,第二次采样由于建立或者保持时间不满足,所以无法确定其是0还是1,不过值得欧慰的是,无论是001,还是011,都至少正确捕捉到了原始信号的变化。也许你会觉得,异步逻辑本身就不可能在时间上被精确地捕捉到,既然能够正确捕捉到信号的变化,那不就够了么?没错,从逻辑上来说是够了,但是从驱动能力上来说,也许不够。大家都知道,FPGA内部的工作电压一般为1.5V,也就是说,理想情况下,逻辑1对应电压1.5V,逻辑0对应电压0V。但是现实是残酷的,逻辑1不可能精确地是1.5V,逻辑0也不可能是精确地0V,事实上,也许业界公认0.5V以上就可以判定为逻辑1,反之则可以被判定为逻辑0,这也是为什么数字信信号比模拟信号更能抗干扰的原因。所以,如果现在问你,FPGA内部有两个触发器的输出都是逻辑1,那么它俩的物理电压相等吗?答案显然是不一定。为什么触发器能够给出正确地输出结果的前提是输入信号要满足其建立、保持时间要求?其实原因很简单,就是要给数字电路以充足的时间来进行充电或者放电操作,从而让其输出的逻辑1更接近于1.5V,逻辑0更接近于0V;反之,如果一个逻辑电平1、0对应的物理电平更接近于1.5V或者0V,那么它就更容易在规定时间内对其后级触发器进行充分的充电或放电控制,从而使其后级触发器的输出也更加"强壮"。那么现在,我们在回过头来审视unsafesignal的物理电压,由于输出unsafesignalf的触发器,很可能出现建立或保持时间要求不满足的情况,因此,在这种情况下,它的充、放电操作都很不充分,输出的逻辑1或者逻辑0的物理电压都不会太好。例如,如果unsafesignalj为逻辑1,那么其物理电压很可能为0.6V,如果当前时钟域中有很多地方都用到了unsafesignal,那么0.6V电压的扇出能力显然会比较差,因此等传递到后续各个用到unsafe signal的地方,物理电压可能就变为0.4V、0.5V、0.55V等等,那么这时 unsafe signal就会被不同的触发器认成不同的逻辑电平,于是错误便诞生了。为了避免这种情况的发生,我们对unsafesignal再次进行采样。由于unsafesignal和safesignal是同步的,因此对于输出safesignal的触发器来说,建立时间已经远远超出了其建立时间要求,因此,即使0.6V的电压对其充电比较慢,但由于充电时间足够,充电电流也有保障,所以也能让safesignal达到一个比较健壮的物理电压,例如1.4V。接下来,我们再将safesignal连接到各个需要使用它的地方,其扇出能力就不会再有任何问题了。”

  1. 2.快时钟域向慢时钟域同步
    第一种简单的方式是在快慢时钟域间添加一个FIFO,这样就可以避免时钟不同步的问题。
    第二种方式是hand shanking机制,简单来说就是一种握手机制,其时序图可以用下图表示:

    在数据有效后,主机发起同步请求req,直至检测到从机的ACK信号后,req拉低,标志一次同步结束。而req信号在从机进行采样同步,并经过两拍后,从机对主机的DATA信号进行采样同步,完成从快时钟域到慢时钟域的数据同步。
    代码如下:
  `// ************************ ***************************************

// Copyright (C) xx Coporation 
// File name: hand_shanking.v  
// Author: Dongyang  
// Date: 2024-11/16 
// Version: 1.0  
// Abstract: CDC multi bit sync,use hand shanking to sync data
//***************************************************************** `
`timescale 1 ns/1 ns
module  hand_shanking_module# (
    parameter integer DATA_WIDTH = 8
    )
(
    input                            i_clk_f          ,     //
    input                            i_sys_rst_n      ,     //外部异步复位信号
    input       [DATA_WIDTH-1 : 0]   i_src_data       ,     //外部输入信号,
    input                            i_src_data_valid ,     //数据有效标志


    input                            i_clk_s          ,
    output                           o_des_ack        ,     //应答完成信号,取上升沿后可作为i_clk_s 的o_des_data 的valid 信号
    output  reg [DATA_WIDTH-1 : 0]   o_des_data            //在i_clk_s 时钟域同步后的信号
);

//******************** siganl define  ***********************
    reg                         r_src_req             ;
    reg                         r_src_ack_sync1       ;
    reg                         r_src_ack_sync2       ;
    
    reg                         r_des_req_sync1       ;
    reg                         r_des_req_sync2       ;
    
    reg                         r_des_ack             ; 

//************** combination  logic *************************
assign        o_des_ack = r_des_ack;
// step 1 , generate r_src_req 
always @(posedge i_clk_f or negedge i_sys_rst_n) begin
    if (~i_sys_rst_n) begin
        r_src_req <= 1'b0;
    end
    else begin
        if (i_src_data_valid) begin                  //once datavalid , generate r_src_req
            r_src_req <= 1'b1;
        end
        else if (r_src_ack_sync2) begin             // when i_clk_s domain ack successfully,r_src_req reset
            r_src_req <= 1'b0;
        end
    end
end

//step 2 and 3, under i_clk_s domain, sync r_src_req from i_clk_f domain,generate ack ok siganl
always @(posedge i_clk_s or negedge i_sys_rst_n) begin
    if (~i_sys_rst_n) begin
        r_des_req_sync1 <= 1'b0;
        r_des_req_sync2 <= 1'b0;
        r_des_ack       <= 1'b0;
    end
    else begin
        r_des_req_sync1 <= r_src_req ;
        r_des_req_sync2 <= r_des_req_sync1;
        r_des_ack       <= r_des_req_sync2;
    end
end
//step 3, once r_des_req_sync2 set, sync o_des_data
always @(posedge i_clk_s or negedge i_sys_rst_n) begin
     if (~i_sys_rst_n) begin
        o_des_data <= 'b0;
     end
     else begin
        if(r_des_req_sync2) begin
            o_des_data <= i_src_data;
        end
     end
end


//step 4 ,sync r_des_ack to i_clk_f domain
always@(posedge i_clk_f or negedge i_sys_rst_n) begin
    if(~i_sys_rst_n) begin
        r_src_ack_sync1 <= 1'b0;
        r_src_ack_sync2 <= 1'b0;
    end
    else begin
        r_src_ack_sync1 <= r_des_ack;
        r_src_ack_sync2 <= r_src_ack_sync1;

    end
end

endmodule

TestBench:

`timescale 1 ns/1 ns
module tb_hand_shanking();

parameter integer DATA_WIDTH =  8;

reg                         clk_f      = 'b0;
reg                         clk_s      = 'b0;
reg                         sys_rst_n  = 'b0;
reg   [DATA_WIDTH- 1 : 0 ]  src_data   = 'b0;
reg                         data_valid = 'b0;

always # 10   clk_f = ~ clk_f;
always # 30   clk_s = ~ clk_s;

initial begin
    clk_f      = 'b0;
    clk_s      = 'b0;
    sys_rst_n  = 'b0;
    src_data   = 'b0;
    data_valid = 'b0;
    #50
    sys_rst_n <= 1'b1;
    #100
    src_data <= 8'h5A;
    data_valid <= 1'b1;
    #20
    data_valid <= 1'b0;
    #500
    src_data <= 8'h6A;
    data_valid <= 1'b1;
    #20
    data_valid <= 1'b0;

end

hand_shanking_module  
#(
        .DATA_WIDTH(DATA_WIDTH)
)
 U_hand_shanking_module_0 
(
    .i_clk_f             (clk_f),
    .i_sys_rst_n         (sys_rst_n),
    .i_src_data          (src_data),
    .i_src_data_valid    (data_valid),
    .i_clk_s             (clk_s),
    .o_des_ack           (),
    .o_des_data          ()
 
);

endmodule

仿真波形:
https://img2024.cnblogs.com/blog/3539410/202411/3539410-20241116161229825-134001601.png