Skip to content

前端扩展

作者:杨村长

一、概念原理

前端扩展的概念

前端依赖库是指开发者自行开发或引入的组件、逻辑,用于满足定制化需求和功能扩展。这些库可被用于自定义界面样式、交互效果、组件功能,或者整合特定的第三方服务(例如微信SDK、钉钉SDK),并且能够被导入到IDE平台中供开发使用。

img

前端扩展开发流程

前端扩展开发通常包括以下几个步骤:

  1. 通过脚手架工具lcap创建项目
  2. 创建全局逻辑或组件并完成开发
  3. 发布到目标CodeWave平台
  4. 在CodeWave项目中导入依赖库并使用它们

前端扩展API与元数据

与传统前端开发中直接引入第三方依赖库不同,CodeWave开发平台中通过前端扩展的方式引入第三方依赖库:

image-20250512115028731

在开发扩展组件时,除了组件代码之外,我们还需要编写一个api.ts文件,该文件为CodeWave平台提供组件API描述:

image-20250512115339818

例如下面示例为MyButton组件声明了两个属性label和type,一个事件my-click:

image-20250507114650007

执行创建步骤时,编译器会转换api.ts为nasl.extention.json,该文件是为CodeWave平台准备的声明文件:

image-20250507115017441

这样用户就可以通过可视化方式与组件交互了:

image-20250507115616196

二、使用场景

开发扩展组件

如果自带组件不能满足需求,或者希望封装业务组件提效,都可以通过开发扩展组件的方式提升平台能力边界。

案例1:封装echarts

ECharts是开源的一个数据可视化库,它可以通过简单的配置实现各种图表的绘制,包括折线图、柱状图、饼图、雷达图等。

我们可以通过封装echarts定制开发特定的图表组件:

img

案例2:封装动态表单

在数据录入、调查问卷、业务流程等特殊场景下,我们通常需要动态定制表单,此时封装一个动态表单组件会变得相当有用:

image-20250506183145287

体验地址

封装三方组件

组织内部通常有常用的组件或组件库,通过封装这些组件并适当编写平台接口即可将三方组件引入平台使用,从而提升开发效率和体验。

案例1:封装element-ui组件

平台已经封装了大量element-ui组件共用户直接使用,我们同样可以参照这些组件继续封装其他组件以丰富团队资产,提升开发效率。

image-20250506193902090

案例2:封装bytemd

下面案例展示了封装三方组件bytemd实现平台内编辑、预览markdown内容:

img

案例3:封装fullcalendar

下面案例展示了通过封装三方库fullcalendar实现个人日程管理功能:

image-20250507101507973

开发前端扩展逻辑

当我们需要编写一些前端可复用逻辑或封装JS-SDK时,例如引入lodash或封装钉钉JS-SDK时,可以通过开发前端扩展逻辑的方式实现。

案例:封装钉钉JS-SDK

下面案例通过封装JS-SDK为前端扩展逻辑,从而可以便捷的调用这些钉钉开放出来的能力:

image-20250507113043673

通过“调用逻辑”组件调用扩展逻辑,它们被分类在“系统逻辑”下:

image-20250507113635592

基础组件二开

Todo:案例待补充

Vue3、React扩展组件支持

平台当前组件支持情况

  • Vue2:Cloud UI(pc, h5)、element-ui(pc, h5)
  • Vue3:element-plus(pc, h5)
  • React:antd(pc)

Todo:案例待补充

三、实操演示

开发环境准备

在开始扩展组件或逻辑开发之前,首先需要安装脚手架工具lcap,安装步骤如下:

  1. 安装脚手架工具:pnpm i -g lcap
  2. 创建项目:lcap init
  3. 安装依赖:pnpm i
  4. 创建组件:lcap create component
  5. 启动项目:pnpm dev

当前演示的lcap版本为v0.5.4,要求提前安装Node.js v18+和pnpm

依赖库发布

前面创建的组件可以尝试发布到CodeWave平台测试,步骤如下:

  1. 设置目标平台地址:lcap config set platform xxx

    xxx替换为目标平台地址,例如https://cunzhang.codewave.163.com

  2. 设置认证信息:

    1. 设置授权令牌:lcap config set authorization xxx

      authorization为管理员登录成功之后授权令牌,可以通过浏览器“应用程序”面板“Cookie”下获取:

      image-20250507121346215

    2. 设置组件版本号:pnpm version 1.0.0

  3. 发布组件:pnpm release

  4. 引入组件:在CodeWave平台“更多配置 - 依赖库管理”导入已发布组件

调试技巧

如果每次更新组件后都需要执行前面发布、导入步骤会非常繁琐,下面介绍利用Chrome插件XSwitch简化调试过程,它会拦截组件资源的http请求到本地开发服务器,从而简化掉以上步骤。具体操作如下:

  1. 检查构建器版本pnpm list @lcap/builder要求最低版本1.40

    如果低于1.4.0则需要先升级构建器,升级方式为:pnpm add @lcap/builder@latest -D

  2. 添加脚本到package.json并执行(如果已经有了可忽略此步骤):

json
{
  "scripts": {
      "watch": "lcap-scripts watch"
  }
}
  1. 安装Chrome浏览器代理插件XSwitch,然后修改插件配置:
json
{
  "proxy": [
    [
      "[PLATFORM_URL]/api/v1/asset-center/library/[name]/version/[version]",
      "http://127.0.0.1:8080/api/library/schema"
    ],
    [
      "[STATIC_URL]/packages/extension/[name]@[version]",
      "http://127.0.0.1:8080"
    ],
  ],
}

各参数含义如下:

  • PLATFORM_URL替换为IDE地址
  • name替换为依赖库的名称
  • version替换为依赖库版本号
  • STATIC_URL可通过浏览器控制台输入window.appInfo.STATIC_URL在浏览器控制台查看

下面是一个实际用例:

json
{
  "proxy": [
    [
      "https://cunzhang.codewave.163.com/api/v1/asset-center/library/my_full_calendar/version/1.0.0",
      "http://127.0.0.1:8080/api/library/schema"
    ],
    [
      "https://lcap-official-bucket.codewave.163.com/packages/extension/my_full_calendar@1.0.0",
      "http://127.0.0.1:8080"
    ]
  ]
}

完成以上配置后每次修改组件代码后刷新浏览器即可查看最新的修改

​ ❌ 注意:调试过程中请勾选“绕过以转到网络”,以避免Chrome浏览器的Service workers缓存,导致刷新之后不更新最新组件代码

image-20250506170732112

基础知识储备

概述:View Component API

用户开发的组件可通过View Component API描述其API或适配平台的各种能力,主要包括:

  • 组件描述:@Component用于组件面板识别排列
  • 扩展组件:@ExtensionComponent用于描述扩展组件类型、IDE用途等
  • 组件属性:@Prop即组件参数
  • 组件事件:@Event标注组件支持的事件
  • 组件插槽:@Slot描述组件插槽
  • 组件实例方法:@Method描述组件提供的方法
  • 组件实例属性:@Prop描述组件提供的可访问属性

整体配置如下:api.ts

typescript
/// <reference types="@nasl/types" />
namespace nasl.ui {
  /* 组件描述 */
  @ExtensionComponent({})
  @Component({})
  export class ElButton extends ViewComponent {
    constructor(options?: Partial<ElButtonOptions>) {
      super();
    }
    /* 组件实例方法 */
    @Method({})
    startLoading(): void {}
    /* 组件实例属性 */
    @Prop({})
    disabled: nasl.core.Boolean;
  }

  export class ElButtonOptions extends ViewComponentOptions {
    /* 组件属性 */
    @Prop({})
    size: nasl.core.String;

    /* 组件事件 */
    @Event({})
    onClick: (event: {}) => any;

    /* 组件插槽 */
    @Slot({})
    slotDefault: () => Array<ViewComponent>;
  }
}

组件定义@Component

使用@Component来标注,用于组件面板识别排列。效果如下:

img

接口配置:api.ts

依赖库中的组件无需定义 group ,默认将依赖库名称作为其分组。

typescript
@Component({
  title: '按钮',                         // 组件显示名称
  icon: 'button',                       // 组件显示图标 (平台内置)
  description: '常用的操作按钮。',         // 组件描述
  /**
   * 组件分组,目前有
   * Display 展示 Form 表单 Selector 选择器 Layout 布局
   * Container 容器 Navigation 导航 Table 表格
   */
  group: 'Display',          // 依赖库使用 依赖库名称作为分组名称           
})

自定义 icon 请参考文档自定义组件面板图标

扩展组件定义@ExtensionComponent

使用@ExtensionComponent来标注扩展组件类型和IDE用途等。例如:

  • 定义type为h5、pc、both之一,可以组件应用于指定环境
  • 定义ideusage可以指定组件用途为元素、容器或弹出类型
接口配置:api.ts

依赖库中的组件无需定义 group ,默认将依赖库名称作为其分组。

typescript
@ExtensionComponent({
  type: 'both', // h5|pc|both
  ideusage: {
    idetype: 'container', // element|container|modal|drawer|messager|popover
  },
})

自定义 icon 请参考文档自定义组件面板图标

组件属性@Prop

可以使用@Prop来描述组件在平台中可配置参数的表单。

img

配置示例如下:

typescript
@Prop({
    // 分组,目前支持 '数据属性' | '主要属性' | '交互属性' | '状态属性' | '样式属性' | '工具属性'
    group: '主要属性',
    // 属性配置面板显示名称
    title: '尺寸',
    // hover 到名称上显示具体描述
    description: '尺寸',
    // 属性设置器,支持参数设置器,请查看 “属性设置器”
    setter: { },
})
size: nasl.core.String = 'small';  // small默认值 'small', nasl.core.String 参数类型,
示例:按钮label属性

下面以按钮组件label属性为例演示扩展组件属性的声明

img

img

接口配置:api.ts
typescript
/// <reference types="@nasl/types" />
namespace nasl.ui {
  /* 省略其他部分 */
  /* 组件选项声明 */
  export class ElButtonOptions extends ViewComponentOptions {
    /* label属性声明 */
    @Prop({
      title: '文本标签',
      description: '按钮文本标签,默认值为按钮',
      setter: {
        concept: 'InputSetter'
      }
    })
    label: nasl.core.String = '按钮';
  }
}
扩展知识

属性输入设置setter可以控制平台收集用户输入的方式。

img

目前可用的设置器有:设置器使用文档

  • InputSetter:输入框设置器(默认)
  • SwitchSetter:开关设置器
  • EnumSelectSetter:下拉选项设置器
  • CapsulesSetter:胶囊设置器
  • NumberInputSetter:数字设置器
  • IconSetter:图标设置器
  • ImageSetter:图片设置器
  • PropertySelectSetter:属性选择设置器
  • AnonymousFunctionSetter:匿名函数设置器

示例:下拉选项设置属性

某些属性在静态绑定时,想要快速的选择效果,这时可以用 TS 的字符串枚举表示选项值,并配合枚举型 Setter 使用。但对应的 NASL 类型仍为nasl.core.String

typescript
@Prop({
    title: '样式类型'
    setter: {
        concept: 'EnumSelectSetter',
        options: [{ title: '默认' }, { title: '浅色' }, { title: '成功色' }, { title: '警告色' }, { title: '危险色' }],
    },
    ...
})
color: 'default' | 'light' | 'success' | 'warning' | 'danger' = 'default';

目前枚举型的 Setter 主要有两种:EnumSelectSetterCapsulesSetter

其他设计器用例

img

img

typescript
Prop({
  group: '主要属性',
  title: '文本',
  description: '按钮内容',
  setter: { concept: 'InputSetter' },
})
text: nasl.core.String = '';

@Prop({
  group: '状态属性',
  title: 'Disabled',
  description: '是否禁用状态',
  setter: { concept: 'SwitchSetter' },
})
disabled: nasl.core.Boolean = false;

@Prop({
  group: '主要属性',
  title: '图标',
  description: '图标类名',
  setter: { concept: 'IconSetter' },
})
icon: nasl.core.String;

@Prop({
  title: '图标位置',
  description: '设置图标居左或居右显示',
  setter: {
    concept: 'EnumSelectSetter',
    options: [{ title: '左' }, { title: '右' }],
  },
})
iconPosition: 'left' | 'right' = 'left';

组件事件 @event

扩展组件事件可以通过@event定义给平台使用,用户可以通过“事件”面板定义特定事件的响应。

示例:按钮自定义点击事件

下面以按钮组件点击事件为例演示扩展组件事件的声明

img

接口配置:api.ts
typescript
/// <reference types="@nasl/types" />
namespace nasl.ui {
  /* 省略其他部分 */
  /* 组件选项声明 */
  export class ElButtonOptions extends ViewComponentOptions {
    /* 点击事件声明 */
    @Event({
      title: '点击事件',
      description: '点击事件',
    })
    onMyClick: (e: { timestamp: nasl.core.Integer }) => void; // my-click
  }
}
扩展知识
  1. 事件名称关键点:on开头,小驼峰命名,组件内事件名为肉串命名,例如onMyClick事件,组件内对应my-click事件,即this.$emit("my-click")

  2. 参数类型:

    1. api.ts中声明类型可以使用nasl.core包中的7种原始类型或nasl.collection包中的两种集合类型

img

img

    1. 参数传一个,需要多个参数可以用对象形式:

img

组件插槽 @slot

通过@slot装饰器定义组件插槽,平台会识别并显示槽位,以便用户向组件添加自定义内容。

示例:面板插槽

下面以面板组件内容和标题插槽为例演示扩展组件插槽的声明,定义了两个插槽:

  • 标题插槽slotTitle
  • 内容插槽slotDefault

img

接口配置:api.ts

typescript
/// <reference types="@nasl/types" />
namespace nasl.ui {
  /* 省略其他部分 */
  /* 组件选项声明 */
  export class MyPanelOptions extends ViewComponentOptions {
    @Slot({
      title: '内容定制',
      description: 'xxxx'
    })
    slotDefault: () => Array<nasl.ui.ViewComponent>;// 需要写成nasl.ui.ViewComponent

    @Slot({
      title: '标题定制'
    })
    slotHeader: () => Array<nasl.ui.ViewComponent>;// 需要写成nasl.ui.ViewComponent
  }
}

扩展知识

  • idetype为 container的组件才可以配合slot插入子节点;
typescript
// 在 @Component 上增加ide 配置
@ExtensionComponent({
  // ...
  ideusage: {
    idetype: 'container',
  }
})
  • 插槽中定义snippets可以让用户向插槽中添加预设的模板代码
typescript
@ExtensionComponent({
  // ...
  ideusage: {
    idetype: 'container',
    structured: true, // 必须设置为 true 才会出现 + 按钮
  }
})
export class ElMenuOptions extends ViewComponentOptions {
  // ...
  @Slot({
    title: 'Default',  
    description: '内容', 
    snippets: [ // ide 点击 “+” 后的提示,title提示描述,code点击后默认添加代码
      {
        title: '添加下拉菜单分组',
        code: '<el-menu-item-group></el-menu-item-group>',
      },
      { title: '菜单项', code: '<el-menu-item></el-menu-item>' },
    ],
  })
  slotDefault: () => Array<nasl.ui.ViewComponent>;
}

组件实例方法 @Method

使用@Method来描述组件提供的方法:

img

接口配置:api.ts

typescript
namespace nasl.ui {
  // 实例方法定义在组件类中
  @Component({})
  @ExtensionComponent({})
  export class MyTable extends ViewComponent {
    @Method({
        title: '导出',
        description: '导出 excel 文件',
    })
    exportExcel(
        // 如果有参数则需要@Param对参数进行描述
        // 参数描述中的 title 和 description 在IDE中不可见,但仍然建议将其作为标识为参数补全。
        @Param({
            title: '当前页码',
            description: '当前页码',
        })
        page: nasl.core.Integer = 1,
        @Param({
            title: '每页条数',
            description: '每页条数',
        })
        size: nasl.core.Integer = 2000,
        // ... 
    ): void {}
  }
}

组件实例属性 @Prop

组件的可访问属性是指在逻辑或表达式的可视化编辑面板中能够直接被访问和操作的组件属性。

img

功能实现

通过组件同步sync:state事件来同步内部状态。

typescript
// sync.ts
// 用于动态生成watch和computed配置项
import type { ComponentOptions, WatchOptionsWithHandler } from 'vue';

export type SyncOption = string | { [name: string]: string | (() => any) };
export interface SyncOptionItem {
    name: string;
    stateKey: string;
    computed?: () => any;
}

function createWatch(name: string): WatchOptionsWithHandler<any> {
    return {
        handler(val, oldVal) {
            if (val === oldVal) {
                return;
            }
            this.$emit('sync:state', name, val);
        },
        immediate: true,
    };
}

function normalizeSyncOptions(options: SyncOption[]) {
    const syncMap: { [key: string]: SyncOptionItem } = {};
    const computedMap: { [key: string]: () => any } = {};
    const watchMap: Record<string, WatchOptionsWithHandler<any>> = {};

    options.forEach((option) => {
        if (typeof option === 'string') {
            syncMap[option] = {
                name: option,
                stateKey: option,
            };
            watchMap[option] = createWatch(option);
            return;
        }

        Object.keys(option).forEach((name) => {
            const val = option[name];

            if (typeof val === 'function') {
                const stateKey = [name, 'sync'].join('__');
                syncMap[name] = {
                    name,
                    stateKey,
                };

                watchMap[stateKey] = createWatch(name);
                computedMap[stateKey] = val;
                return;
            }

            syncMap[name] = {
                name,
                stateKey: val,
            };
            watchMap[val] = createWatch(name);
        });
    });

    return {
        syncMap,
        computedMap,
        watchMap,
    };
}

export default (...options: SyncOption[]) => {
    const {
        syncMap,
        watchMap,
        computedMap,
    } = normalizeSyncOptions(options);
    return {
        watch: {
        ...watchMap,
        },
        computed: {
        ...computedMap,
        },
    } as ComponentOptions<any>;
};

组件内部引入并设置

typescript
// comp.vue
import sync from './sync';

export default {
    mixins: [
        sync({
            disabled: 'disabled',
        }),
    ],
}

可选项:在 api.ts文件中通过为相关属性设置settable: true可开启属性可赋值

typescript
@Prop({·
  //...
  settable: true,
})

数据源

开发案例

案例1:封装echarts

效果演示
组件开发
接口声明

案例2:封装fullcalendar

效果演示

fullcalendar

组件开发

安装依赖

js
import FullCalendar from '@fullcalendar/vue'
import timeGridPlugin from '@fullcalendar/timegrid'
import dayGridPlugin from '@fullcalendar/daygrid'
import listPlugin from '@fullcalendar/list'
import interactionPlugin from '@fullcalendar/interaction'
import zhCN from '@fullcalendar/core/locales/zh-cn'

step1:引入fullcalendar并能够正常显示

  • 组件
vue
<template>
  <FullCalendar ref="calendar" :options="calendarOptions">
    <!-- 如果显式传递了eventContent插槽或者在设计器环境下对外暴露一个插槽用于配置事件布局 -->
    <template v-if="$scopedSlots.eventContent || $env.VUE_APP_DESIGNER"  #eventContent="arg">
      <slot name="eventContent" v-bind="arg"></slot>
    </template>
  </FullCalendar>
</template>

<script>
import FullCalendar from '@fullcalendar/vue'
import timeGridPlugin from '@fullcalendar/timegrid'
import dayGridPlugin from '@fullcalendar/daygrid'
import listPlugin from '@fullcalendar/list'
import interactionPlugin from '@fullcalendar/interaction'
import zhCN from '@fullcalendar/core/locales/zh-cn'
import './assets/iconfont.css'

var instance;

export default {
  components: {
    FullCalendar // make the <FullCalendar> tag available
  },
  props: {
    initialEvents: {
      type: Array,
      default: () => []
    },
    dataSource: {
      type: [Array, Object, Function]
    },
    height: {
      type: [Number, String],
      default: null
    },
  },
  watch: {
    dataSource: {
      handler() {
        this.$nextTick(() => {
          this.loadDataSource();
        });
      },
      immediate: true
    },
    height(newVal) {
      this.$nextTick(() => {
        this.$refs.calendar.getApi().setOption('height', newVal);
      });
    },
  },
  data() {
    return {
      calendarOptions: {
        plugins: [timeGridPlugin, dayGridPlugin, listPlugin, interactionPlugin],
        // 初始视图
        height: this.height,
        initialView: 'timeGridWeek',
        headerToolbar: {
          left: 'today prev title next',
          right: 'timeGridWeek,dayGridMonth,listMonth'
        },
        // 定义dayHeaderContent
        dayHeaderDidMount: function (arg) {
          const viewType = arg.view.type;
          if (viewType !== 'listMonth') {
            const el = arg.el;
            const date = arg.date;
            const weekday = arg.text.substr(-2);
            const div = document.createElement('div')
            const p2 = document.createElement('p')
            p2.innerText = weekday
            p2.classList.add('weekday')
            div.appendChild(p2)
            if (viewType === 'timeGridWeek') {
              const p1 = document.createElement('p')
              p1.innerText = date.getDate()
              p1.classList.add('date')
              div.appendChild(p1)
            }
            el.innerText = ''
            el.appendChild(div)
          }
        },
        // 添加hover到event时操作按钮
        eventDidMount: function (arg) {
          if (arg.view.type === 'listMonth') {
            const el = arg.el.lastChild
            const event = arg.event
            const span = document.createElement('span')
            span.classList.add('event-operation')
            span.style.visibility = 'hidden'
            const i1 = document.createElement('i')
            const i2 = document.createElement('i')
            i1.classList.add('iconfont', 'icon-edit')
            i2.classList.add('iconfont', 'icon-delete')
            i1.addEventListener('click', e => {
              e.stopPropagation()
              // 触发编辑事件
              instance.$emit('event-edit', event)
            })
            i2.addEventListener('click', e => {
              e.stopPropagation()
              // 触发删除事件
              instance.$emit('event-delete', event)
            })
            span.appendChild(i1)
            span.appendChild(i2)
            el.appendChild(span)
          }
        },
        eventMouseEnter: function (arg) {
          const el = arg.el
          const oper = el.querySelector('.event-operation')
          if (oper) oper.style.visibility = 'visible'
        },
        eventMouseLeave: function (arg) {
          const el = arg.el
          const oper = el.querySelector('.event-operation')
          if (oper) oper.style.visibility = 'hidden'
        },
        locale: zhCN,
        // 显示当前时间点指示器
        nowIndicator: true,
        // 可拖选
        select: this.handleSelect,
        selectable: true,
        // 事件相关设置
        eventColor: '#dee6f4',
        eventTextColor: '#333',
        eventClick: this.handleEventClick,
        events: []
      }
    }
  },
  mounted() {
    instance = this
  },
  methods: {
    handleSelect: function (arg) {
      this.$emit('select', arg)
    },
    handleEventClick: function (arg) {
      this.$emit('event-click', arg.event)
    },
    addEvent: function (arg) {
      this.$refs.calendar.getApi().addEvent(arg)
    },
    deleteEvent: function (arg) {
      const api = this.$refs.calendar.getApi()
      api.getEventById(arg.id).remove()
    },
    normalizeData(data) {
      // 如果dataSource存在,使用dataSource
      if (Array.isArray(data)) {
        return data;
      }
      if (typeof data === 'object' && Array.isArray(data.list)) {
        return data.list;
      }
      // 如果没设置dataSource,使用initialEvents
      if (!this.dataSource && this.initialEvents) {
        return JSON.parse(JSON.stringify(this.initialEvents))
      }
      // 都没有则设置为空数组
      return [];
    },
    async loadDataSource() {
      if (typeof this.dataSource === 'function') {
        const data = await this.dataSource({});
        this.calendarOptions.events = this.normalizeData(data);
      } else {
        this.calendarOptions.events = this.normalizeData(this.dataSource);
      }
    },
    async reload() {
      return this.loadDataSource();
    },
  },
}
</script>

设置样式

vue
<style>
:root {
  --fc-button-bg-color: #f0f2f5;
  --fc-button-text-color: #333;
  --fc-button-border-color: #f0f2f5;
  --fc-button-hover-bg-color: #f0f2f5;
  --fc-button-hover-border-color: #f0f2f5;
  --fc-button-active-bg-color: #f0f2f5;
  --fc-daygrid-day-bg-color: #fff;
}

.fc .fc-button-primary:not(:disabled).fc-button-active:focus,
.fc .fc-button-primary:not(:disabled):active:focus {
  box-shadow: none;
}

.fc .fc-button-primary,
.fc .fc-button-primary:hover {
  background: #fff;
  border-color: #e2e2e2;
  color: #333;
  cursor: pointer;
}

.fc .fc-button-primary:not(:disabled).fc-button-active,
.fc .fc-button-primary:not(:disabled):active {
  background: rgba(65, 195, 204, 0.2);
  border-color: #01afb4;
  color: #01afb4;
  cursor: pointer;
}

.fc .fc-toolbar-title {
  display: inline-block;
  font-size: 1.2rem;
  font-weight: 600;
  color: #333;
  margin: 0;
  padding: 0;
  line-height: 1.2;
  vertical-align: middle;
}

.fc-col-header-cell .fc-scrollgrid-sync-inner {
  display: none;
}

.fc-day .weekday {
  margin: 10px 0 5px;
}

.fc-day .date {
  width: 50px;
  height: 50px;
  color: #afadad;
  font-size: 1.2rem;
  font-weight: 600;
  line-height: 50px;
  text-align: center;
  margin: 0 auto 10px;
}

.fc-day-today .date {
  background-color: #01afb4;
  border-radius: 50%;
  color: #fff;
}

.fc-timegrid-event .fc-event-main {
  padding-left: 7px;
  border-left: 4px solid #88abda;
}

.fc-timegrid-axis,
.fc-timegrid-slot-label {
  background-color: #f0f2f5;
}

.event-operation {
  float: right;
}

.event-operation .iconfont {
  font-size: 24px;
  cursor: pointer;
}

.fc .fc-list-table td {
  line-height: 24px;
}
</style>

接口编写

ts
/// <reference types="@nasl/types" />
namespace extensions.lib_example.viewComponents {
  const { Component, Prop, ViewComponent, Slot, Method, Event, ViewComponentOptions } = nasl.ui;

  @ExtensionComponent({
    type: 'pc',
    ideusage: {
      idetype: 'container',
      // structured: true,
    }
  })
  @Component({
    title: '日程管理',
    description: '日程管理',
  })
  export class MyFullCalender<T> extends ViewComponent {
    constructor(options?: Partial<MyFullCalenderOptions<T>>) {
      super();
    }

    @Method({
      title: '添加事件',
      description: '添加事件方法,传递的参数为事件对象,形如:{title: "事件标题", start: new Date(), end: new Date(), startStr: "2023-01-01", endStr: "2023-01-02", color: "#000000", allDay: false}',
    })
    addEvent(event: T): void { };

    @Method({
      title: '删除事件',
      description: '删除事件方法,传递的参数为事件对象,形如:{title: "事件标题", start: new Date(), end: new Date(), startStr: "2023-01-01", endStr: "2023-01-02", color: "#000000", allDay: false}',
    })
    deleteEvent(event: T): void { };

    @Method({
      title: '重新加载数据',
      description: '重新加载数据'
    })
    reload(): void { }
  }

  export class MyFullCalenderOptions<T> extends ViewComponentOptions {
    @Prop({
      group: '主要属性',
      title: '初始日程列表',
      description: '日程列表,形如:[{title: "事件标题", start: "2024-10-14T09:00", end: "2024-10-14T09:00"}]',
    })
    initialEvents: nasl.collection.List<T>;

    // 数据源属性
    @Prop({
      group: '数据属性',
      title: '数据源',
      description: '展示数据的输入源,可设置为数据集对象或者返回数据集的逻辑',
      docDescription: '列表展示的数据。数据源可以绑定变量或者逻辑。变量或逻辑的返回值可以是数组,也可以是对象。对象格式为{list:[], total:10}',
    })
    dataSource: { list: nasl.collection.List<T>; total: nasl.core.Integer } | nasl.collection.List<T>;

    @Prop({
      group: '数据属性',
      title: '数据类型',
      description: '数据源返回的数据结构的类型,自动识别类型进行展示说明',
      docDescription: '列表每一行的数据类型。该属性为展示属性,由数据源推倒得到,无需填写。',
    })
    dataSchema: T;

    // 高度属性
    @Prop({
      group: '主要属性',
      title: '高度',
      description: '组件的高度,可设置为数字,单位为像素,也可以设置为css字符串,如"100px"、"100%",如果设置为auto, 则高度将根据内容自动调整。',
      docDescription: '组件的高度,可设置为数字,单位为像素,也可以设置为css字符串,如"100px"、"100%",如果设置为auto, 则高度将根据内容自动调整。',
    })
    height: nasl.core.Integer | nasl.core.String;

    @Event({
      title: '点击日程',
      description: '点击日程触发,可用于获取日程信息',
    })
    onEventClick: (event: {
      id: nasl.core.String;
      title: nasl.core.String;
      start: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
      end: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
      startStr: nasl.core.String;
      endStr: nasl.core.String;
      color: nasl.core.String;
      allDay: nasl.core.Boolean;
    }) => void;

    @Event({
      title: '选择日程区间',
      description: '选择日程区间之后触发的事件,可获取起始和结束时间,用于创建新的日程',
    })
    onSelect: (event: {
      start: nasl.core.Date | nasl.core.DateTime;
      end: nasl.core.Date | nasl.core.DateTime;
      startStr: nasl.core.String;
      endStr: nasl.core.String;
      allDay: nasl.core.Boolean;
    }) => void;

    @Event({
      title: '点击修改日程',
      description: '点击修改日程触发的事件,可用于获取日程信息',
    })
    onEventEdit: (event: {
      id: nasl.core.String;
      title: nasl.core.String;
      start: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
      end: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
      startStr: nasl.core.String;
      endStr: nasl.core.String;
      color: nasl.core.String;
      allDay: nasl.core.Boolean;
    }) => void;

    @Event({
      title: '点击删除日程',
      description: '点击删除日程触发的事件,',
    })
    onEventDelete: (event: {
      id: nasl.core.String;
      title: nasl.core.String;
      start: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
      end: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
      startStr: nasl.core.String;
      endStr: nasl.core.String;
      color: nasl.core.String;
      allDay: nasl.core.Boolean;
    }) => void;

    @Slot({
      title: '自定义事件内容展示',
      description: '自定义事件内容展示',
    })
    slotEventContent: (current: {
      event: {
        id: nasl.core.String;
        title: nasl.core.String;
        start: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
        end: nasl.core.Date | nasl.core.DateTime | nasl.core.String;
        startStr: nasl.core.String;
        endStr: nasl.core.String;
      }
    }) => Array<nasl.ui.ViewComponent>;
  }
}

案例3:封装动态表单

案例4:封装钉钉JS-SDK

案例5:基础组件二开