探索实现kintone Lookup自动更新的新方式

cybozu发表于:2023年06月02日 15:17:54更新于:2023年06月12日 14:41:01

Index

前言

众所周知,Lookup 是 kintone  中一个比较好用的功能。它可以在填写数据的时候,复制别的 app  的字段值,从而可以避免大块数据重复手动录入的问题。

但是,在日常的运用中,常常有一个需求被广泛提及,那就是 kintone 提供的 Lookup  所复制的数据是固定的,不会随参照源的改变而改变,而有时候,用户就是希望数据会随着改变。这时,所谓的  【Lookup    自动更新】功能便被提了出来。

其实,已经有不少的开发者成功开发出了【Lookup 自动更新】功能,有些是专门定制的自定义开发,有些是基于一些泛用插件的功能来实现。但是,通过最近的研究,我们发现这些方法都存在一些问题,在研究就这些问题的过程中,得到了一些开发的经验,于是想在这篇文章中同大家分享。

明确共识

在展开讲述之前,我们应该了解【Lookup 自动更新】方面的基础概念。

首先,我们要知道,kintone 默认 提供的 Lookup  功能,没有自动更新,并不是功能上的缺陷,而是最开始功能上就是这么定义的。因为所谓的【Lookup 自动更新】只是一部分场景的需求,而很多时候,我们就是不希望进行自动更新,自动更新对于这些场景来说是错误的表现。

举例来说,假设现在有两张表,一张商品表,一张订单表,客户买东西时,会生成订单表,订单表里会有客户所选的商品,包括商品名,商品价格等信息。在这种场景下,我们就不能用【Lookup 自动更新】来更新商品价格,因为订单所反映的是成交时的商品价格,但是商品价格会随时间的变化而变化,不能因为这个变化而影响了之前已经产生过的订单。

那么,另一方面,什么时候我们会需要用到【Lookup 自动更新】功能呢?总的来说,如果用户只关心被参照数据的最新数据,而不想知道历史数据的时候,就会用到【Lookup 自动更新】功能了。

举例来说,也是其他相关文章经常提到的一个案例。一张是客户表,是被  Lookup 参照的 app,我们称它为 A。另一张是案件表,Lookup  客户表的字段是客户名,需要复制的数据有客户电话,客户地址等。 我们称它为 B。当 B 的一条记录生成后,他所参照的客户的电话发生了改变,当再次查看  B  的记录时,希望看到的是最新变化后的电话号码,因为以前的电话号码,用户已经不关心了。

引出问题

我研究了市面上能找到的介绍【Lookup 自动更新】功能的各种文章,发现他们都存在一些难以解决的问题。要讲清楚这些问题,先要了解它们基本的设计思路。

就用上面的【客户-案件】案例来说,设计思路是这样的:

  1. 对 A 表进行自定义。

  2. 当 A 的记录发生改变时,找到 B 中所有参照 A 这条记录的记录,并更新它们。

然后,这时候需求增加,有一个叫 C 的 App 也希望能参照 A,于是,开发人员编辑对 A  的自定义,在找参照的地方模仿 B,同样的逻辑, 再更新一遍 C。

现在,有的读者可能已经发现问题了:

  1. 如果 A 是一个常用 master 表,例如【员工表】,公司中可能有大量的 App 在 Lookup 它的值。要知道【员工表】可能是人事部门或IT部门在管理的,每当有别的同事制作新 App 需要参照【员工表】并需要自动更新时,必须通过【员工表】的管理员进行自定义代码的改动,当这种改动变得频繁起来,跨部门的沟通和代码冗长容易造成各种各样的问题。

  2. B、C 这样的表一旦改动,或者删除,A 也需要进行相应的改动。繁琐只是一部分问题,更可怕的是,改动 B、C 的人(新接手管理的人)可能根本不知道有【Lookup 自动更新】的功能,因为自定义并不存在于参照方,从 B、C 这里根本看不出有自定义的痕迹。

  3. B、C 这样的表一旦多起来,性能可能也会成为问题。因为改一条 A 的记录,会牵涉到很多记录的查询和更新。

说到这里,产生这种问题的原因也应该呼之欲出了。可以说设计思路一开始就存在缺陷。A 相当于一个内容的发布者,B、C 相当于内容阅读者。如果 A 发布了某个内容的两个版本,那 B、C 需要什么版本不应该由 A     来分发,而是应该他们自己来订阅。

新思路

为了描述更准确一些,在下文中,我们将 A 类的表叫做【主表】,B、C  类的表叫做【事务表】

根据上文我们已经总结好的问题症结,现在可以拓展新的思路去实现相似的功能效果。大致的设计框架如下:

  1. 所有的自定义开发程序,写在【事务表】中,【主表】中不写入任何自定义程序

  2. 【事务表】中的 Lookup 字段,以及需要复制的字段,即需要表示【主表】相应字段的最新数据的,不做真正的数据更新操作,而只是在有需要显示的时候,才去参照【主表】的内容。

优缺点

下面对这种设计框架进行解读:1  带给我们的好处是不必每次都劳烦【主表】的管理员,可谓解决了之前提出的主要问题。但是随之带来的是,【主表】更新的时机,不可能被【事务表】中的程序所探知,于是在  2  中,我们放弃了真正的数据更新,改为在显示时去实时参照【主表】中的内容。这样做的利弊有:

  • 所有数据维持 kintone 默认下的 Lookup 仕样,即数据生成时的原始状态。当我们撤销自定义时,一切都会恢复原状,避免了数据不整合的风险。

  • 绝大多数的情况下,都是查看网页上内容时,需要看到【主表】的最新内容,所以即使原始数据不改动,只是实时参照的话,也足够应付。

  • 由于原始数据没有真正改动,所以所有的汇总统计都是不会是根据【主表】最新数据所产生的,所以有统计需求的场景,就不要采用这种设计框架了。

更进一步

有了上面的思路,已经可以动手开发了,但是我们应该可以更有追求一点。

考虑一下,当如下需求产生时,我们应该如何应对?

  • 实时参照的 Lookup 看上去的确不错,我有很多应用都想这么做

  • Lookup 的字段,和需要复制的字段中,有一些想看原始数据,另一些则想看最新数据

我们发现这些需求是那种从特殊定制运用到广泛适用的那种情况,这时候就想到,plugin 不正是 kintone  为了解决通用性开发问题而提供的开发模式吗?在这里用插件是再合适不过了。

开发插件必定要产生更多的准备工作和更多的代码规模,如果您只是想实现单一功能,可以从之后的代码逻辑舍去插件相关部分,可以大大缩短开发进程。

开发过程

现在我们已经明确了要做一个什么样的东西,下面应该就可以进入到正式开发的环节了。

俗话说,万事开头难。由于本次开发中,运用到的技术相对较多,如果一开始没有规划好的话,很难保证后面的开发能顺利进行。所以这里有必要对开发前的准备,做一番说明。

开发准备

开发准备中,我把内容分为三个方面,可行性验证,开发环境,和 kintone App。

可行性验证

在正式开发之前,我们还需要确认一下本次开发的功能是可以实现的,几个最关键的节点,逻辑是可以连通的。如果事先不进行验证的话,可能开发到一半才发现功能实现不了,就像空中楼阁是注定建不成的。

1. 字段类型为 lookup 的字段可否知道?它所引用的【主表】的字段名可否知道?

通过寻找 REST API 我们发现,【获取表单的设置】这个 API  可以达到要求。它会返回字段设置的各种信息,其中也包括 Lookup  的关联应用信息。所以结论为可行。

2. 在【事务表】详细页面或者列表页面,是否能够拿到 lookup  的关联信息,即和【主表】关联的记录号?

根据【事务表】中的 Lookup 字段,找到【主表】中的参照字段。说简单点就是寻找  Lookup 的目标。经过一定的研究发现,在 REST API中并没有提供相应的信息,你获取的  Lookup  中,只回有当前的值,而不会有参照的目标。但是这点根本难不倒我们,因为我们发现,网页上  Lookup 是能链接到参照的那条记录的(类似 /k/88/show#record=1  这样的形式),所以根据超链接地址,我们便可拿到想要的 Record  ID。所以结论为可行。

有了以上两点的验证,我们确保项目是可行的。

开发环境

由于本次的开发内容还是略有点复杂的,所以一个纯 js  文件一镜到底的模式是万万不可取的。我默认读者已经具有搭建 npm  包管理项目经验,之后所有的叙述也是基于这个基础之上。如果您对这方面不是很了解的话,可以参看之前发布的    环境搭建的系列文章。

下面我会讲一下本次开发中运用到的技术,以及为什么会这样选择的缘由。

插件。  在接下来的开发过程介绍中,我会以插件开发为例来讲述。所以如果有读者对插件开发不熟悉的话,可以先看参这两篇文章:    插件开发的准备, 插件开发流程

Vue。  因为涉及到插件开发,在配置页面,必定会产生画面的制作,并且会产生前后台的数据交换逻辑。这时候我们采用前端框架制作画面上的一部分元素,会是一个不错的选择。当然这里选择  React 也是很推荐的。有需求的读者自行替换即可。

TypeScript。  对于本项目要不要上  TypeScript,我也是有所犹豫。因为 TypeScript  向来争议比较大,虽然在提高代码质量方面十分优秀,但是也带来了开发难度直线上升的问题。这里最后还是选择迎难而上,一方会面的原因是,开发中会遇到  API 获取 App  配置的部分,这部分的数据的结构有点复杂,把类型定义清晰之后,可以避免一些处理数据时的失误,并把主要精力放在逻辑上,而不是数据类型比对上。另一方面,开发者网站本应主打开拓引导,提供先例等方面的宗旨,况且之前虽然推出过  TypeScript  介绍的文章,但是还没有实际开发的案例文章,所以本文就斗胆做一次尝试。希望对将来有意向运用  TypeScript 进行开发的个人或团队提供一些参考。

运用 TypeScript  肯定会产生一些前期配置工作,幸好之前我们有过几篇文章的介绍,有需要的读者可以参考一下。

环境小结:开发环境的搭建,在项目建立的初期是比较花时间的。但是一个好的环境对项目的帮助也是非常之大,可以节省后期很多时间。当然上面说的这些技术也不是必须引入的,开发者可以根据实际情况,酌情考虑,或弃用或寻找替代品都是可以的。

kintone App

这次我们还是用之前讲到的案件管理项目来作为范例。主表我们采用kintone商城里自带的【客户信息】应用。

而事务表我们选择自行创建,取名【案件管理】。因为演示并不需要太多的字段,所以这里简单地加几个字段。

这里的【对方客户】字段设置成 Lookup  类型,用来引用【客户信息】应用中的信息。而【客户电话】字段,是复制【客户信息】中的电话,但需要显示最新电话,而不是创建时的电话。

在【客户信息】应用中录入一条数据,电话现在写的是  88880000,注意,这是给旧电话准备的数据。

在【案件管理】应用中也添加一条数据,对方客户里点击搜选。

电话号码 88880000 已经复制过来了。

然后再把客户信息的电话号码改成 99990000,模拟数据发生了变化。

最后,我们期待的目标是,【案件管理】中的【客户电话】变成了 99990000。

到这里,我们的前期准备工作已经全部完成了,虽然过程可能比较长,但是扎实的准备一定能为后面的开发打下坚实的基础的。

代码解读

项目创建好之后,大概的目录结构长这个样子

红框中的 src  目录中的内容为具体的插件代码,其他文件则是一些项目运行的配置文件。读者如果对项目配置不熟悉的话,上文中已经提到过,可以参考本网站的开发环境搭建的系列文章,这里不作赘述。

src 中文件结构基本上是按插件的推荐配置摆放的,下面对 js  文件夹中的文件大致负责的功能做一个介绍。

  • config.ts
                         插件配置页面的总入口,负责传递【PLUGIN_ID】给创建好的 Vue 组件

  • ConfigPage.vue
                    Vue 的组件文件。负责插件配置页面的画面摆放和逻辑交互。

  • desktop.ts
                    用户使用 App 时起作用。负责读取插件配置信息,和主要功能的实现。

  • DesktopPage.vue
                    Vue 的组件文件。可以在列表画面或者详细画面做一些自定义的UI,譬如做一个 switch 按钮来切换新老数据的展示。不包括在本次项目代码解说中。

  • fields.d.ts
                    补充了一些 kintone 公用字段的类型信息等。本项目未必为用到,作为通用文件存在。

  • lookup.ts
                    定义了 lookup 类型的外壳,并包装了获取 lookup 参照信息的 API。

下面我们逐个解读代码。请对照代码中的注释来理解实现的功能。

       config.html    

<!-- 配置画面的HTML模板 -->
<section class="settings">
  <h2 class="settings-heading">启用LookUp实时参照最新数据</h2>
  <p class="kintoneplugin-desc">这里将列出所有Form中的LookUp字段,请选择您要启用实时参照的项目</p>
  <!-- 给配置内容预留一个容器,取id为main,config.ts中会用到 -->
  <div id="main"></div>
</section>

       config.ts    

import { createApp } from 'vue'
import ConfigPage from './ConfigPage.vue'
// 获取plugin id,读取和保存时配置信息时都需要用到
const pluginId = kintone.$PLUGIN_ID
// 获取模板中所预留的容器
const main = document.getElementById('main') as HTMLDivElement
// 创建vue组件ConfigPage,并传给它plugin id
const app = createApp(ConfigPage, { pluginId })
// 挂在组件到main
app.mount(main)

       lookup.ts     

import { KintoneRestAPIClient } from '@kintone/rest-api-client'
// 定义了一个Lookup类型,其中给lookup设置的是对象类型
// 下面还有展开,到具体用的时候还会定义的更详细,这里够用了
export type Lookup = {
  label: string
  code: string
  lookup: object
}
// 创建sdk客户端
const client = new KintoneRestAPIClient({
  baseUrl: 'https://yourdomain.cybozu.cn',
})
// 获取一个应用中所有类型是Lookup的字段
export const getFormSetting = async () => {
  const prop = (await client.app.getFormFields({ app: kintone.app.getId() as number })).properties
  const lookUpFields = Object.values(prop).filter((f) => 'lookup' in f) as Lookup[]
  return lookUpFields
}

       ConfigPage.vue     

<template>
  <!-- 一个循环获取Lookup字段的模板 -->
  <div v-for="i in data.saveConfig" :key="i.label" class="config-row">
    <span class="toggle-wrapper">
      <input :id="`css${i.label}`" v-model="i.checked" type="checkbox" />
      <label class="toggle" :for="`css${i.label}`"><span class="toggle-handler"></span></label>
    </span>
    <span class="lookup-field-name-text-parent">
      <span class="control-label-gaia lookup-field-name-text">{{ i.label }}</span>
    </span>
  </div>
  <div class="kintoneplugin-row">
    <button type="button" class="js-cancel-button kintoneplugin-button-dialog-cancel" @click="cancel">Cancel</button>
    <button type="submit" class="kintoneplugin-button-dialog-ok" @click="save">Save</button>
  </div>
</template>
<script setup lang="ts">
/* eslint-disable no-console */
import { reactive, onMounted } from 'vue'
import { getFormSetting, Lookup } from './lookup.ts'
// 接受外面传递过来的pluginId
const props = defineProps<{ pluginId: string }>()
// 用到的数据声明,并且是响应式的
const data: { config: Lookup[]; saveConfig: Array<{ label: string; checked: boolean; code: string }> } = reactive({
  config: [],
  saveConfig: [],
})
onMounted(async () => {
  const gettedConfig = kintone.plugin.app.getConfig(props.pluginId)
  data.saveConfig = JSON.parse(gettedConfig.setting)
  // 拿到所有是Lookup的字段
  const res: Lookup[] = await getFormSetting()
  // 比对上次保存的结果,并确定哪些勾选哪些不勾选
  const cp = res.map((i) => {
    for (let j = 0; j < data.saveConfig.length; j += 1) {
      if (data.saveConfig[j].code === i.code) {
        return { code: i.code, label: i.label, checked: data.saveConfig[j].checked }
      }
    }
    return { code: i.code, label: i.label, checked: false }
  })
  data.saveConfig = cp
})
// 按下保存按钮时的操作:用plugin的API来保存到kintone内
function save() {
  kintone.plugin.app.setConfig({ setting: JSON.stringify(data.saveConfig) })
}
// 按下取消按钮时的操作:退回到plugin默认页面
function cancel() {
  window.location.href = `../../${kintone.app.getId()}/plugin/`
}
</script>
// 引入css
<style>
@import '../css/config.css';
</style>

       config.css     

config画面的css,主要是开关按钮的效果

.settings-heading {
  font-size: 1.8em;
}
.config-check-box {
  width: 1.2em;
  height: 1.2em;
}
.config-row {
  height: 4.6em;
}
.lookup-field-name-text-parent {
  position: relative;
  top: -0.64em;
  margin-left: 1.2em;
}
.lookup-field-name-text {
  font-size: 1.5em;
}
.toggle-wrapper {
  overflow: hidden;
  transform: translate3d(-50%, -50%, 0);
}
.toggle-wrapper input {
  position: absolute;
  left: -99em;
}
.toggle {
  cursor: pointer;
  display: inline-block;
  position: relative;
  width: 120px;
  height: 50px;
  background: #d21626;
  border-radius: 5px;
  transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.toggle::before,
.toggle::after {
  position: absolute;
  line-height: 50px;
  font-size: 14px;
  z-index: 2;
  transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.toggle::before {
  content: 'OFF';
  left: 20px;
  color: #d21626;
}
.toggle::after {
  content: 'ON';
  right: 20px;
  color: #fff;
}
.toggle-handler {
  display: inline-block;
  position: relative;
  z-index: 1;
  background: #fff;
  width: 65px;
  height: 44px;
  border-radius: 3px;
  top: 3px;
  left: 3px;
  transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
  transform: translateX(0);
}
input:checked + .toggle {
  background: #66b317;
}
input:checked + .toggle::before {
  color: #fff;
}
input:checked + .toggle::after {
  color: #66b317;
}
input:checked + .toggle .toggle-handler {
  width: 54px;
  transform: translateX(60px);
  border-color: #fff;
}

       desktop.ts     

定义和通用方法:

import { KintoneRestAPIClient } from '@kintone/rest-api-client'
import { getFormSetting, Lookup } from './lookup.ts'
const pid = kintone.$PLUGIN_ID
const client = new KintoneRestAPIClient({
  baseUrl: 'https://yourdomain.cybozu.cn',
})
kintone.events.on('app.record.edit.submit', (event: KintoneEvent) => {
  return event
})
// 需要更详细的结构,所以扩充了Lookup类型
type SpecificLookup = Lookup & {
  lookup: {
    relatedApp: { app: string; code: string } // 所参照的app
    fieldMappings: Array<{ field: string; relatedField: string }> // 同时复制的字段
    relatedKeyField: string // 本lookup字段的字段代码
  }
}
/**
 * 根据所给的需要参照的app,给出该条记录的信息。record的编号则需要通过解析href得到
 * @param targetApp
 * @param hle
 * @returns
 */
async function getTargetRecord(targetApp: string, hle: HTMLAnchorElement) {
  const targetRecordId = (hle.href.match(/record=(\d+)/) as string[])[1]
  const targetRecord = await client.record.getRecord({ app: targetApp, id: targetRecordId })
  return targetRecord
}

在详细页面做的自定义:

// 在详细页面做的事
kintone.events.on('app.record.detail.show', async (event) => {
  // 先拿到插件的设定,表示哪些lookup字段需要适用实时参照功能
  const savedConfig: Array<{ label: string; checked: boolean; code: string }> = JSON.parse(
    kintone.plugin.app.getConfig(pid).setting,
  )
  // 拿到本app的form信息,从中只拿lookup字段的信息
  const lookupSetting: SpecificLookup[] = (await getFormSetting()) as SpecificLookup[]
  // 比对form信息和插件设定信息,从而得到一个真正需要起作用的lookup字段们
  // 我不需要得到比设定内容更多的lookup,即使新增了lookup字段,却还未在插件中设定过,反正默认他们也是disable,所以以savedConfig为基础循环比较好
  const enabledLookupSetting = savedConfig.map((i) => {
    for (let j = 0; j < lookupSetting.length; j += 1) {
      if (i.checked && lookupSetting[j].code === i.code) {
        // saveConfig中只有最基础的信息,而我们在这里将要使用很多lookup中的设定信息,比如参照的app,其他要复制的字段的对应关系等,所以这里返回SpecificLookup类的东西
        return lookupSetting[j]
      }
    }
    return undefined
  })
  // 对这些lookup字段们进行循环
  for (let i = 0; i < enabledLookupSetting.length; i += 1) {
    // 简化变量名且帮助ts检查空
    const els = enabledLookupSetting[i]
    if (els) {
      const code = els.code as string
      // 拿到looup的html元素,希望它是一个<a>即HTMLAnchorElement,但其实并不一定,因为如果是空,kintone给出的结构将跳过<a>这一层,直接给下一层的<span>
      const anchorEle = (kintone.app.record.getFieldElement(code) as HTMLElement).firstChild
      // 如果不是<a>,即没有参照信息,接下来所有的操作都可以跳过了,这个判断也cover了href为空的可能性
      if (anchorEle && anchorEle instanceof HTMLAnchorElement) {
        const targetRecord = await getTargetRecord(els.lookup.relatedApp.app, anchorEle)
        // 正常情况下,spanEle 会是一个 instance of HTMLSpanElement,但万一是其他,反正textContent也应该拿得到,这里就先不加断言
        const spanEle = anchorEle.firstChild
        if (spanEle && spanEle.textContent) {
          // 把lookup所参照的值赋值给元素
          // 注意,这里不能用赋值给event.record;return event;来做,因为kookup类型的字段,kintone不支持通过event来改变值
          // 得到值的写法比较繁琐,这是由于所参照的app的字段代码,在lookup的设定对象嵌套比较深
          // 另一种考虑是不再获取sapn,直接给anchorELe赋值,也能达到差不多的效果,优点是简单,缺点是改变了结构,span消失了
          spanEle.textContent = targetRecord?.record[els.lookup.relatedKeyField].value as string
          // 其他要复制的字段也要把参照值复制过来
          // 写在这个if下的原因是,如果lookup的值不是参照来的,那为了数据一致性,其他要赋值的字段最好也不要显示参照的值
          els.lookup.fieldMappings.map((mapping) => {
            // 拿到参照字段的元素
            const refEl = (kintone.app.record.getFieldElement(mapping.field) as HTMLElement).firstChild
            if (!refEl || !refEl.textContent) return null
            // 拿到要复制的值
            const overwriteValue = targetRecord?.record[mapping.relatedField].value
            // 判断一下需要复制的值是什么类型的,一般是文本框之类的都是string
            // 但是如果是用户选择类型之类的话,就需要根据具体情况,特殊设计了
            if (typeof overwriteValue === 'string') {
              refEl.textContent = overwriteValue as string
            }
            return null
          })
        }
      }
    }
  }
  return event
})

在列表页面的自定义

// 在列表页面要做的事
kintone.events.on('app.record.index.show', async (event) => {
  const savedConfig: Array<{ label: string; checked: boolean; code: string }> = JSON.parse(
    kintone.plugin.app.getConfig(pid).setting,
  )
  const lookupSetting: SpecificLookup[] = (await getFormSetting()) as SpecificLookup[]
  const enabledLookupSetting = savedConfig.map((i) => {
    for (let j = 0; j < lookupSetting.length; j += 1) {
      if (i.checked && lookupSetting[j].code === i.code) {
        return lookupSetting[j]
      }
    }
    return undefined
  })
  for (let i = 0; i < enabledLookupSetting.length; i += 1) {
    const els = enabledLookupSetting[i]
    if (els) {
      const code = els.code as string
      // 拿到表格中的td元素们
      const tdEls = kintone.app.getFieldElements(code)
      tdEls?.map(async (tdEl) => {
        const anchorEl = tdEl.querySelector('a') as HTMLAnchorElement
        const targetRecord = await getTargetRecord(els.lookup.relatedApp.app, anchorEl)
        // Lookup元素的值改为关联字段的值
        anchorEl?.textContent &&
          (anchorEl.textContent = targetRecord?.record[els.lookup.relatedKeyField].value as string)
      })
    }
  }
  return event
})

       fields.d.ts

declare namespace kintone {
  const $PLUGIN_ID: string
}
declare namespace kintone.types {
  interface Fields {}
  interface SavedFields extends Fields {
    [key: string]: { type: string; value: string | number | boolean }
    $id: kintone.fieldTypes.Id
    $revision: kintone.fieldTypes.Revision
    更新人: kintone.fieldTypes.Modifier
    创建人: kintone.fieldTypes.Creator
    更新时间: kintone.fieldTypes.UpdatedTime
    创建时间: kintone.fieldTypes.CreatedTime
    key: kintone.fieldTypes.RecordNumber
  }
}
interface KintoneEvent {
  record: kintone.types.SavedFields
}

编写完代码,调试没有问题之后,我们看到了效果。详细画面的电话号码已经显示为最新的【99990000】了。

友情提示

  • 上方展示的代码只是一种示范例子,有些地方可能并不完美。读者可根据自身需求进行改造。

  • 建议加一个总体切换的按钮,在最新数据和原始数据直接随意切换。这样可以让用户时时刻刻意识到自己在看什么样的数据。

  • 在列表页面,范例中只做到了 Lookup 字段自身的改变,复制的字段,还请读者自行完成。

  • 项目工程化后的代码,可以在这里查看。这里要声明的是,给出的代码只是为了示例和演示,请读者务必在理解的基础上进行开发和改造,切不可全部照搬,更不建议直接当产品使用。我们不保证示例代码的长久可用性。

结束语

本篇文章对 Lookup 要查看最新数据的需求进行了分析,并提出与传统做法不同的,只有看数据时才改变显示数据的新思路。一方面可以解决 Lookup 一直以来的一种需求,另外这种思路也可为其他的开发场景提供一些启发。