列表视图、卡片视图切换

在日常项目开发中,我们可能会遇到当前视图是个表格,通过某个操作按钮将它变成卡片的形式.
列表视图、卡片视图切换
列表视图、卡片视图切换

这篇文章将带大家实现这种功能。通过上面两张图片可以看到出来不管是表格还是卡片,它都在当前的视图里面,所以我们需要一个视图容器来包裹表格卡片,并且表格可以使用平台默认的表格渲染,我们只需要自定义卡片即可。

我们用资源模块下面的国家菜单来实现这么一个功能这是对应的URL:

http://localhost:8080/page;module=resource;viewType=TABLE;model=resource.ResourceCountry;action=resource%23%E5%9B%BD%E5%AE%B6;scene=resource%23%E5%9B%BD%E5%AE%B6;target=OPEN_WINDOW;menu=%7B%22selectedKeys%22:%5B%22%E5%9B%BD%E5%AE%B6%22%5D,%22openKeys%22:%5B%22%E5%9C%B0%E5%9D%80%E5%BA%93%22,%22%E5%9C%B0%E5%8C%BA%22%5D%7D

源码下载

views

创建外层的视图容器

刚刚我们讲过,不管是表格还是卡片,它都在当前的视图里面,所以我们需要写一个视图容器来包裹它们,并且对应的容器里面允许拆入表格卡片,我们先创建TableWithCardViewWidget.ts

// TableWithCardViewWidget.ts 
import { BaseElementWidget, SPI, Widget } from '@kunlun/dependencies';
import TableWithCardView from './TableWithCardView.vue';

enum ListViewType {
  TABLE = 'table',
  CARD = 'card'
}

@SPI.ClassFactory(
  BaseElementWidget.Token({
    widget: 'TableWithCardViewWidget'
  })
)
export class TableWithCardViewWidget extends BaseElementWidget {
  @Widget.Reactive()
  private listViewType: ListViewType = ListViewType.TABLE; // 当前视图展示的类型,是展示卡片还是表格

  public initialize(props) {
    if (!props.slotNames) {
      props.slotNames = ['tableWidget', 'cardWidget'];
    }
    super.initialize(props);
    this.setComponent(TableWithCardView);

    return this;
  }
}

TableWithCardViewWidget中的initialize函数中,我们定义了两个插槽: tableWidget、cardWidget,所以需要在对应的vue文件里面里接收这两个插槽

<template>
  <div class="list-view-wrapper">
    <!-- 表格插槽 -->
    <div style="height: 100%" v-if="listViewType === 'table'">
      <slot name="tableWidget" />
    </div>

    <!-- 卡片插槽 -->
    <div v-if="listViewType === 'card'">
      <slot name="cardWidget"></slot>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  props: ['listViewType', 'onChangeViewType'],
  inheritAttrs: false,
  setup(props, context) {
    const onChangeViewType = (listViewType) => {
      props.onChangeViewType(listViewType);
    };
  }
});
</script>

这样一来,我们就定义好了视图容器,接下来就是通过自定义layout的方式注册该容器

layout注册

import { registerLayout, ViewType } from '@kunlun/dependencies';

registerLayout(
  `<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" cols="4" slot="search"/>
        </view>
    </pack>
    <pack widget="group" slot="tableGroup" style="position: relative">
      <element widget="actionBar" slot="actionBar" slotSupport="action">
        <xslot name="actions" slotSupport="action" />
      </element>
      <element widget="TableWithCardViewWidget">
        <template slot="tableWidget">
          <element widget="table" slot="table" datasource-provider="true">
            <element widget="expandColumn" slot="expandRow" />
            <xslot name="fields" />
            <element widget="rowActions" slot="rowActions" />
          </element>
        </template>
        <template slot="cardWidget">
          <element widget="CardListViewWidget" datasource-provider="true" />
        </template>
      </element>
    </pack>
  </view>
`,
  {
    moduleName: 'resource.ResourceCountry',
    actionName: 'resource#国家',
    viewType: ViewType.Table
  }
);

这个layout是基于平台默认的table layout改造的,大家可以看到

      <element widget="TableWithCardViewWidget">
        <template slot="tableWidget">
          ...
        </template>
        <template slot="cardWidget">
             ...
        </template>
      </element>

这段模版是将自定义的视图容器TableWithCardViewWidget注册进去,并且有两个template, 每个template里面的slot属性其实就是在TableWithCardViewWidget中的initialize函数定义的两个插槽: tableWidget、cardWidget,这两个名字要对应上。

<template slot="tableWidget">
  <element widget="table" slot="table" datasource-provider="true">
    <element widget="expandColumn" slot="expandRow" />
    <xslot name="fields" />
    <element widget="rowActions" slot="rowActions" />
  </element>
</template>

第一个slot是 tableWidget,内部是默认的表格layout,所以在运行时的时候,会渲染平台默认的表格组件

<template slot="cardWidget">
    <element widget="CardListViewWidget" datasource-provider="true" />
 </template>

第二个slot是 cardWidget,里面渲染的是 CardListViewWidget, 所以这个时候我们需要按照自定义视图的方式自定义CardListViewWidget即可。

自定义卡片

// CardListViewWidget.ts

import {
  ActiveRecord,
  BaseElementListViewWidget,
  BaseElementWidget,
  Condition,
  DEFAULT_TRUE_CONDITION,
  ISort,
  Pagination,
  QueryContext,
  queryPage,
  QueryVariables,
  SPI,
  Widget
} from '@kunlun/dependencies';
import cardList from './card-list.vue';

@SPI.ClassFactory(
  BaseElementWidget.Token({
    widget: 'CardListViewWidget'
  })
)
export class CardListWidget extends BaseElementListViewWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(cardList);
    this.viewModel = props.model as string;
    return this;
  }

  @Widget.Reactive()
  public viewModel: string = '';

  @Widget.Reactive()
  public condition: string = '';

  @Widget.Reactive()
  public setCondition() {
    this.condition === '1==1' ? (this.condition = '2==2') : (this.condition = '1==1');
  }

  public async queryPage<T = ActiveRecord>(
    condition: Condition,
    pagination: Pagination,
    sort: ISort[],
    variables: QueryVariables,
    context: QueryContext
  ): Promise<any> {
    const model = this.metadataRuntimeContext.model;
    this.loading = true;
    const result = await queryPage(
      model.model,
      {
        currentPage: pagination.current,
        pageSize: this.showPagination ? pagination.pageSize : -1,
        sort,
        condition: condition.toString() === DEFAULT_TRUE_CONDITION ? '' : condition
      },
      undefined,
      variables,
      {
        maxDepth: 0
      }
    );
    this.loading = false;
    return result;
  }
}
<template>
  <div v-if="showDataSource && showDataSource.length">
    <div v-for="data in showDataSource" :key="data.id">
      {{ data.id }}
    </div>
  </div>

  <div v-else>暂无数据</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch } from 'vue';

export default defineComponent({
  mixins: [ManualWidget],
  props: {
    viewModel: {
      type: String,
      default: ''
    },
    loading: {
      type: Boolean,
      default: false
    },
    pagination: {
      type: Object
    },
    showDataSource: {
      type: Array
    },
    refreshProcess: {
      type: Function
    },
    onPaginationChange: {
      type: Function
    }
  },
  components: { OioPagination, OioSpin },
  setup(props) {
    return {};
  }
});
</script>

当卡片对应的widget写完后,我们还需要一个切换卡片跟表格的功能。

视图类型切换

我们只需要在TableWithCardViewWidget对应的vue里面添加切换视图类型的功能就行了。

<template>
  <div class="list-view-wrapper">
    <!-- 切换视图类型 -->
    <button @click="onChangeViewType(listViewType === 'table' ? 'card' : 'table')">
      {{ listViewType === 'table' ? '切换成卡片' : '切换成表格' }}
    </button>
    <!-- 表格插槽 -->
    <div style="height: 100%" v-if="listViewType === 'table'">
      <slot name="tableWidget" />
    </div>

    <!-- 卡片插槽 -->
    <div v-if="listViewType === 'card'">
      <slot name="cardWidget"></slot>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  props: ['listViewType', 'onChangeViewType'],
  inheritAttrs: false,
  setup(props, context) {
    const onChangeViewType = (listViewType) => {
      props.onChangeViewType(listViewType);
    };
  }
});
</script>

最后在TableWithCardViewWidget.ts里面写对应的onChangeViewType方法即可.

public resetSearch() {
    getRouterInstance()!.push({
      segments: [
        {
          path: 'page',
          parameters: {
            searchBody: undefined,
            currentPage: undefined
          },
          extra: { preserveParameter: true }
        }
      ]
    });
  }

  @Widget.Method()
  public onChangeViewType(viewType: ListViewType, init: boolean): void {
    this.listViewType = viewType;
    this.reloadDataSource(undefined);
    this.reloadActiveRecords(undefined);
    // 重置搜索,如果有需要就放开
    // this.resetSearch();
    if (!init) {
      const tableWidget = this.dslSlots?.tableWidget?.widgets?.[0];
      if (tableWidget && tableWidget.automatic == null) {
        tableWidget.automatic = false;
      }
      const cardWidget = this.dslSlots?.cardWidget?.widgets?.[0];
      if (cardWidget && cardWidget.automatic == null) {
        cardWidget.automatic = false;
      }
    }
  }

这样一来,我们就完成了所有的功能。

Oinone社区 作者:汤乾华原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/18377.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
汤乾华的头像汤乾华数式员工
上一篇 2024年10月15日 pm5:38
下一篇 2024年10月17日 pm7:43

相关推荐

  • 前端 SPI 注册 + 渲染

    在阅读本篇文章之前,您需要学习以下知识点: 1: TS 结合 Vue 实现动态注册和响应式管理 前端开发者在使用 oinone 平台的时候会发现,不管是自定义字段还是视图,对应的 typescript 都会用到@SPI.ClassFactory(参数),然后在对用的class中重写initialize方法`: @SPI.ClassFactory(参数) export class CustomClass extends xxx { public initialize(props) { super.initialize(props); this.setComponent(FormString); return this; } } 本文将带您熟悉 oinone 前端的 SPI 注册机制以及 TS + Vue 的渲染过程。 不管是自定义字段还是视图@SPI.ClassFactory(参数)都是固定写法,区别在于参数不同,这篇文章里面详细描述了参数的定义。 SPI 注册机制 有自定义过字段、视图经验的开发者可能会发现,字段(表单字段)SPI 注册用的是FormFieldWidget.Token生成对应的参数,视图 SPI 注册用的是BaseElementWidget.Token,那么为什么要这样定义呢? 大家可以想象成现在有一个大的房子,房子里面有很多房间,每个房间都有自己的名字,比如FormFieldWidget就是房间的名字,BaseElementWidget也是房间的名字,这样一来我们就可以根据不同的房间存放不同的东西。 下面给大家展示下伪代码实现: class SPI { static container = new Map<string, WeakMap<object, object>>() static ClassFactory(token) { return (target) => { if(!SPI.container.get(token.type)) { SPI.container.set(token.type, new WeakMap()) } const services = SPI.container.get(token.type) services?.set(token, target) } } } class FormFieldWidget { static Token(options) { return { …options, type: 'Field' } } static Selector(options) { const fieldWidgets = SPI.container.get('Field') if(fieldWidgets) { return fieldWidgets.get(options)! } return null } } @SPI.ClassFactory(FormFieldWidget.Token({ viewType: 'Form', ttype: 'String', widget: 'Input' })) class StringWidget { } // 字段元数据 const fieldMeta = { name: "name", field: "name", mode: 'demo.model', widget: 'Input', ttype: 'String', viewType: 'Form' } // 找到对应的widget const widget = FormFieldWidget.Selector({ viewType: fieldMeta.viewType, ttype: fieldMeta.ttype, widget: fieldMeta.widget, }) 在上述代码中,我们主要是做了这么写事情: 1.SPI class class SPI { static container = new Map<string, WeakMap<object, object>>() } SPI 类是一个静态类,用于管理服务的注册和获取。 container 是一个静态属性,类型是 Map,它的键是字符串,值是 WeakMap。这个结构允许我们为每个服务类型(例如,Field)管理多个服务实例。 2.ClassFactory 方法 static ClassFactory(token) { return (target) => { if…

    2024年9月26日
    1.6K00
  • 自定义字段的数据联动

    某种情况下,开发人员期望自定以的字段发生变化后,需要修改其他的字段,这篇文章从两个维度来讲解如果处理数据的联动 界面设计器配置 1: 在界面设计器页面中的的组件区域找到自定义的字段,设计元件 2: 在模型区域,搜索提交方式,如果找到了,就把该字段拖拽进来, 如果找不到,就在元件中的组件区域,拖拽一个文本字段,按照下面的配置进行配置,然后保存 图一是找到了对应的字段图二是找不到对应的字段 【图一】 【图二】 图二的字段编码必须是constructDataTrigger 3: 从模型区搜索联动函数,将其拖拽进来 3: 从模型区搜索提交数据,将其拖拽进来4: 从模型区搜索提交字段,将其拖拽进来 5: 发布 (记得刷新页面哦) 最后再到对应的设计器页面,选中该字段,进行配置 提交方式为blur或者change , 需要开发者手动调用该方法 this.blur()或者this.change(value) // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // this.change(val) // this.blur() } } 联动函数就是要调用的后端函数 提交数据分为:变更字段 -> 发生变化后的字段当前视图字段 -> 当前视图所有的字段指定字段 -> 指定字段,如果配置的指定字段,那么提交字段的配置就要输入对应的字段 代码配置 平台也支持通过代码的方式修改字段 // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // 修改字段本身的值 this.change(val) // 修改其他字段的值 this.formData.otherField = 'value' this.reloadFormData$.subject.next(true); } }

    2023年11月9日
    1.9K00
  • 左树右表默认选择第一行

    import { BaseElementWidget, Widget, SPI, ViewType, TableSearchTreeWidget } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'tree', model: '改成当前视图的模型' }) ) export class CustomTableSearchTreeWidget extends TableSearchTreeWidget { protected hasExe = false; @Widget.Watch('rootNode.children.length') protected watchRootNode(len) { if (len && !this.hasExe) { this.hasExe = true; const firstChild = this.rootNode?.children?.[0]; if (firstChild) { this.onNodeSelected(firstChild); this.selectedKeys = [firstChild.key]; } } } }

    2024年11月26日
    1.3K00
  • 如何自定义表格字段?

    目录 一、表单字段注册vue组件实现机制 二、表格字段注册vue组件实现机制 三、机制对比分析 四、表格字段完整案例 表单字段注册vue组件实现机制 核心代码 public initialize(props) { super.initialize(props); this.setComponent(VueComponent); return this; } 表格字段注册vue组件实现机制 核心代码 @Widget.Method() public renderDefaultSlot(context: RowContext) { const value = this.compute(context); return [createVNode(CustomTableString, { value })]; } 因为表格有行跟列,每一列都是一个单独的字段(对应的是TS文件),但是每列里面的单元格承载的是Vue组件,所以通过这种方式可以实现表格每个字段对应的TS文件是同一份,而单元格的组件入口是通过renderDefaultSlot函数动态渲染的vue组件,只需要通过createVNode创建对应的vue组件,然后将props传递进去就行 上下文接口 interface RowContext<T = unknown> { /** * 当前唯一键, 默认使用__draftId, 若不存在时,使用第三方组件内置唯一键(如VxeTable使用{@link VXE_TABLE_X_ID}) */ key: string; /** * 当前行数据 */ data: Record<string, unknown>; /** * 当前行索引 */ index: number; /** * 第三方组件原始上下文 */ origin: T; } 机制对比分析 表单字段 vs 表格字段渲染机制对比表 对比维度 表单字段实现方案 表格字段实现方案 绑定时机 initialize 阶段静态绑定 renderDefaultSlot 阶段动态创建 组件声明方式 this.setComponent(Component) createVNode(Component, props) 上下文传递 通过类成员变量访问 显式接收 RowContext 参数 渲染控制粒度 字段级(表单控件) 单元格级 表格字段完整案例 import { SPI, ViewType, BaseFieldWidget, Widget, TableNumberWidget, ModelFieldType, RowContext } from '@kunlun/dependencies'; import CustomTableString from './CustomTableString.vue'; import { createVNode } from 'vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ ttype: ModelFieldType.String, viewType: [ViewType.Table], widget: 'CustomTableStringWidget' }) ) export class CustomTableStringWidget extends BaseTableFieldWidget { @Widget.Method() public renderDefaultSlot(context:RowContext) { const value = this.compute(context); // 当前字段的值 const rowData = context.data // 当前行的数据 const dataSource = this.dataSource // 表格数据 if (value) { // 自定义组件入口在此处 return [createVNode(CustomTableString, { value })]; } return []; } } <template> <div>当前值: {{value}}</div> </template> <script lang="ts"> import { defineComponent } from 'vue'…

    2023年11月6日
    1.9K00
  • 前端环境和启动前端工程

    本节核心是带大家直观的感受下我们上节构建的demo模块,并搭建前端环境为后续学习打下基础 环境准备 配置NPM源 npm config set registry http://nexus.shushi.pro/repository/kunlun/ 登录NPM源账号 npm login –registry "http://nexus.shushi.pro/repository/kunlun/" # username、password、email 获取方式: # 1、请见oinone开源社区群公告,也可以联系oinone合作伙伴或服务人员; # 2、参考数式发过去的部署包(部署.zip)中的账号说明:docker-mvn-npm账号.md npm info underscore 环境准备参考 [前端环境准备Mac版本]https://doc.oinone.top/oio4/9225.html [前端环境准备Windows版本]https://doc.oinone.top/oio4/9226.html 启动前端工程 1、下载前端工程本地运行 [ss-front-modules.zip]ss-front-modules 2、解压下载后的工程,可以查看README.MD快速上手指南; 找到vue.config.js文件,修改devServer.proxy.pamirs.target为后端服务的地址和端口 const WidgetLoaderPlugin = require('@kunlun/widget-loader/dist/plugin.js').default; const Dotenv = require('dotenv-webpack'); module.exports = { lintOnSave: false, runtimeCompiler: true, configureWebpack: { module: { rules: [ { test: /\.widget$/, loader: '@kunlun/widget-loader' } ] }, plugins: [new WidgetLoaderPlugin(), new Dotenv()], resolveLoader: { alias: { '@kunlun/widget-loader': require.resolve('@kunlun/widget-loader') } } }, devServer: { port: 8081, disableHostCheck: true, progress: false, proxy: { pamirs: { // 支持跨域 changeOrigin: true, // 改成本地后端对应的IP和端口; 本地后端未启动的情况也可改成无代码后端IP和端口 target: 'http://192.168.0.121:8190' } } } }; 3、 安装依赖和运行在工程目录ss-front-modules下执行 # 安装依赖 npm i # 运行 npm run dev 4、若安装失败 检查本地node、npm、vue对应的版本 5、 如果启动报错 清除node_modules后重新 npm i mac清除命令:npm run cleanOs windows清除命令: npm run clean 注:要用localhost域名访问,.env文件这里也要改成localhost。如果开发中一定要出现前后端域名不一致,老版本Chrome会有问题,修改可以请参https://www.cnblogs.com/willingtolove/p/12350429.html。或者下载新版本Chrome 进入前端工程ss-front-modules文件目录下,执行 npm run dev,最后出现下图就代表启动成功 6、使用 http://127.0.0.1:8081/login 进行访问,并用admin账号登陆,默认密码为admin 5、点击左上角进行应用切换,会进入App Finder页面,可以看到所有已经安装的应用,可以对照boot的yml配置文件看。 在后续的学习过程中我们会不断完善示例中的模块。至此恭喜您,前端工程已经启动完成。 示例工程分层说明 # ss-boot 不做业务研发,只做包的组装和依赖 # ss-oinone 与Oinone结合层,这个工程结构可以把数式Oinone的改造收口,也是业务工程依赖的核心层 # ss-admin-widget 与界面设计器无代码的结合工程,在这个工程结构里可以把组件放在无代码平台上使用 # ss-project 模拟的项目工程,做某个项目的个性化开发

    2024年5月28日
    2.4K00

Leave a Reply

登录后才能评论