使用Kintone Portal Designer从门户上传附件

betsy_yan发表于:2022年03月15日 16:32:04更新于:2023年09月13日 14:27:00

Index

前言

kintone 集成了向员工显示的公告和常用应用以及应用访问链接等。
例如,制作经费报销应用时,在经费信息中附上收据,员工可以很轻松地提交给会计。
按照原本的做法,需要在门户上设置经费报销应用的链接;跳到经费报销应用画面后,再添加记录;然后将收据的图像作为附件上传;最后保存记录等多个步骤。
但是,对于经常需要报销费用的员工来说,这种方式需要更多的操作,而且比想象中要麻烦得多。
因此,这次我们自定义了门户,可以在门户上进行经费报销,通过将收据拖放上传来减少操作上的麻烦,实现业务改善。

创建经费报销应用

参考以下图片和字段的设置,创建经费报销预付款。



字段类型字段名称字段代码
多行文本框概要description
附件收据receipt
数值费用(不含税)

cost

日期
日期date

Kintone Portal Designer的设置

参考此文(用Kintone Portal Designer来设计门户)来安装 Kintone Portal Designer 。

安装后,转到 kintone 门户,然后单击工具栏上的"</>"按钮以启动。

以下画面显示的内容,请参考编辑 HTML、CSS、JavaScript 并保存各个选项卡上的项目。

编辑 HTML

参考以下内容编辑并保存 HTML 。

/*    
* 使用Kintone Portal Designer从门户上传附件的程序范例    
* Copyright (c) 2021 Cybozu    
*    
* Licensed under the MIT License    
* https://opensource.org/licenses/mit-license.php    
*/    
<form>    
  <div class="parent">    
    <div class="header">    
        <h2>经费报销</h2>    
    </div>      
    <div class="label1">    
      <label for="description">概要&nbsp;</label>    
    </div>    
    <div class="input1">    
        <input type="text" id="description" name="description">    
    </div>    
    <div class="label2">    
        <label for="amount">费用(不含税)&nbsp;</label>    
    </div>    
    <div class="input2">    
        <input type="text" id="amount" name="amount">    
    </div>    
    <div class="label3">      
        <label for="date">日期(YYYY-MM-DD)&nbsp;</label>    
    </div>    
    <div class="input3">    
        <input type="text" id="date" name="date">    
    </div>    
    <div class="drop_zone" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);">    
        <div id="file_name" class="tool_tip">将文件拖放到此处</div>    
    </div>    
    <div class="button">    
        <input type="submit" value="添加" class="bSubmit" onclick="registerExpense(event);">    
    </div>    
  </div>    
</form>


编辑 CSS

参考以下内容编辑并保存 CSS 。

/*
 * 使用Kintone Portal Designer从门户上传附件的程序范例
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/licenses/mit-license.php
*/
.parent {
  display: grid;
  grid-template-columns: repeat(3,300px);
  grid-template-rows: 50px repeat(3,30px) 50px;
  border: 3px solid green;
  border-radius: 15px;
  margin: 10px 10px 10px 10px;
  column-gap: 10px;
  row-gap: 1em;
  background-color: #ffffff;
}
label {
  font-weight: bold;
}
input {
  border: 2px solid green;
  border-radius: 5px;
}
.button {
   grid-area: 5 / 1 / 6 / 4;
   justify-self: center;
   align-self: center;
}
.bSubmit {
   padding: 5px 30px;
   font-weight: bold;
}
.drop_zone {
  border: 2px dotted green;
  border-radius: 10px;
  grid-column: 3 / 4;
  grid-row: 2 / 5;
  margin: 10px 10px;
}
.tool_tip{
  text-align: center;
  padding: 30px 0;
  opacity: 0.5;
}
.header {
  grid-area: 1 / 1 / 2 / 4;
  font-weight: bold;
  font-size: 1.5em;
  padding: 0px 50px;
}
.label1{
  grid-column: 1 / 2;
  grid-row: 2 / 3;
  text-align: right;
}
.label2{
  grid-column: 1 / 2;
  grid-row: 3 / 4;
  text-align: right;
}
.label3{
  grid-column: 1 / 2;
  grid-row: 4 / 5;
  text-align: right;
}
.input1{
  grid-column: 2 / 3;
  grid-row: 2 / 3;
}
.input2{
  grid-column: 2 / 3;
  grid-row: 3 / 4;
}
.input3{
  grid-column: 2 / 3;
  grid-row: 4 / 5;
}


编辑 JavaScript

参考以下内容编辑并保存 JavaScript 。这次我们参考 MDN Web Docs 的“File drag and drop”。

/*
 * 使用Kintone Portal Designer从门户上传附件的程序范例
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/licenses/mit-license.php
*/
let file =  null;
const dropHandler = (ev) => {
  console.log('文件已拖放。');

  // 避免默认操作中打开文件。
  ev.preventDefault();

  if (ev.dataTransfer.items) {
    // 浏览器为Chrome时,使用 DataTransferItemList 接口访问文件。
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
      // 若拖入的项目不是文件,则跳过。
      if (ev.dataTransfer.items[i].kind === 'file') {
        file = ev.dataTransfer.items[i].getAsFile();
        console.log('... file[' + i + '].name = ' + file.name);
      }
    }
  } else {
    // 对于老式浏览器,使用 DataTransfer 接口访问文件。
    for (let i = 0; i < ev.dataTransfer.files.length; i++) {
      file = ev.dataTransfer.files[i];
      console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
    }
  }
  document.getElementById('file_name').innerText = file.name;
};
const dragOverHandler = (ev) => {
  console.log('文件已拖入放置区域。');

  // 避免默认操作中打开文件。
  ev.preventDefault();
};
const APP_ID = {kintone App ID};
const registerExpense = async (ev) => {
  console.log('进入了registerExpense函数内。');
  ev.preventDefault();
  const fileKeys = [];
  const param = {
    'app': APP_ID,
    'record': {
      'description':{
        'value': document.getElementById('description').value
      },
      'cost':{
        'value': document.getElementById('amount').value
      },
      'date':{
        'value': document.getElementById('date').value
      }
    }
  };
  try {
    const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'POST', param);
    // 成功
    console.log(resp);
    console.log(`Record ID:${resp.id}`);
    const rec_id = resp.id;
    console.log(`File:${file}`);
    if (file) {
      uploadFile(rec_id);
    }
    alert(`记录添加成功。记录ID: ${resp.id}`);
    resetForm();
  } catch(error) {
    // 错误
    alert(`记录添加失败。 ${error.message}`)
  }
};
const uploadFile = (rec_id) => {

  let formData = new FormData();
  formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
  
  formData.append('file', file, file.name);

  const url = kintone.api.url('/k/v1/file.json', true);
  let xhr = new XMLHttpRequest();
  xhr.open('POST', url);
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  xhr.onload = () => {
    if (xhr.status === 200) {
      // 成功
      console.log(JSON.parse(xhr.responseText));
      const key = {'fileKey': JSON.parse(xhr.responseText).fileKey};
      updateRecord(rec_id, key);
    } else {
      // 错误
      console.log(JSON.parse(xhr.responseText));
    }
  };
  xhr.send(formData);
};
const updateRecord = async (rec_id, fileKey) => {
  const param = {
    'app': APP_ID,
    'id': rec_id,
    'record': {
      'receipt': {
        'value': [fileKey]
      }
    }
  }
  const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', param);
  // 成功
  console.log(resp);
};
const resetForm = () => {
  document.getElementById('description').value = '';
  document.getElementById('amount').value = '';
  document.getElementById('date').value = '';
  document.getElementById('file_name').innerText = '';
  file = null;
};


操作检查

在 Design Portal 中保存上述设置,通过左上角的开关启用 Design Portal 。

打开 kintone 的门户页面,更新页面后会显示以下画面。

填入必要项目,把经费报销使用到的收据文件拖入放置区域,点击添加按钮,就会在上述创建的经费报销应用中添加新的记录。

若打开经费报销应用,发现添加了记录,则以上操作成功。

显示其他部件

单击 Kintone Portal Designer 的“Export”按钮后,在显示的子菜单中单击“Export as JavaScript(Desktop)”。然后制作的门户设计的 JavaScript 文件将下载到下载文件夹。

接着返回门户页面,点击齿轮图标,点击“kintone系统管理”,进入设置页面。

点击“自定义”—“通过JavaScript / CSS自定义”菜单,进入自定义设置页面。

点击 JavaScript文件(电脑专用)的“通过上传添加”按钮,上传并保存刚才下载的门户设计的 JavaScript 文件。

返回 Kintone Portal Designer 的页面、关闭 Default Portal 的开关。

 再次回到门户页面后,其他部件也会同时显示。

此外,要自定义显示的部件,请单击右上角的“・・・”,选择“门户的设置”。

在"在门户页面显示"中,选择并保存要查看的内容。

代码说明

此函数是当文件拖放到  Class 名为 drop_zone 的 div 元素中时调用的函数。

const dropHandler = (ev) => {
  console.log('文件已拖放。');

  // 避免默认操作中打开文件。
  ev.preventDefault();

  if (ev.dataTransfer.items) {
    // 浏览器为Chrome时,使用 DataTransferItemList 接口访问文件。
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
      // 若拖入的项目不是文件,则跳过。
      if (ev.dataTransfer.items[i].kind === 'file') {
        file = ev.dataTransfer.items[i].getAsFile();
        console.log('... file[' + i + '].name = ' + file.name);
      }
    }
  } else {
    // 对于老式浏览器,使用 DataTransfer 接口访问文件。
    for (let i = 0; i < ev.dataTransfer.files.length; i++) {
      file = ev.dataTransfer.files[i];
      console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
    }
  }
  document.getElementById('file_name').innerText = file.name;
};


这个方法可以防止文件在拖放时打开。

ev.preventDefault();


在“ev.dataTransfer.items”接口里拖拽的所有数据列表,仅在文件类型时获取数据内容。(这里的接口由最新的浏览器 Chrome 等支持。)

if (ev.dataTransfer.items) {
    // 浏览器为Chrome时,使用 DataTransferItemList 接口访问文件。
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
      // 若拖入的项目不是文件,则跳过。
      if (ev.dataTransfer.items[i].kind === 'file') {
        file = ev.dataTransfer.items[i].getAsFile();
        console.log('... file[' + i + '].name = ' + file.name);
      }
    }
  }


在“ev.dataTransfer.files”接口上获取拖放文件的列表。(该接口由老式浏览器支持。)

// 对于老式浏览器,使用 DataTransfer 接口访问文件。
    for (let i = 0; i < ev.dataTransfer.files.length; i++) {
      file = ev.dataTransfer.files[i];
      console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
    }


此代码是文件被拖到 Class 名为 drop_zone 的 div 元素中时所调用的函数。这里也防止文件打开。

const dragOverHandler = (ev) => {
  console.log('文件已拖入放置区域。');

  // 避免默认操作中打开文件。
  ev.preventDefault();
};


此函数将Class 名为 drop_zone 的 div 元素内的文件以及输入的内容作为新记录添加到 kintone 中。另外,请在{kintone App ID}中设置 kintone 创建的经费报销应用的ID。

const APP_ID = {kintone App ID};
const registerExpense = async (ev) => {
  console.log('进入了registerExpense函数内。');
  ev.preventDefault();
  const fileKeys = [];
  const param = {
    'app': APP_ID,
    'record': {
      'description':{
        'value': document.getElementById('description').value
      },
      'cost':{
        'value': document.getElementById('amount').value
      },
      'date':{
        'value': document.getElementById('date').value
      }
    }
  };
  try {
    const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'POST', param);
    // 成功
    console.log(resp);
    console.log(`Record ID:${resp.id}`);
    const rec_id = resp.id;
    console.log(`File:${file}`);
    if (file) {
      uploadFile(rec_id);
    }
    alert(`记录添加成功。记录ID: ${resp.id}`);
    resetForm();
  } catch(error) {
    // 错误
    alert(`记录添加失败。 ${error.message}`)
  }
};


在此代码中,获取经费报销的各字段的值,创建新记录。
这里没有保存文件数据。

const param = {
    'app': APP_ID,
    'record': {
      'description':{
        'value': document.getElementById('description').value
      },
      'cost':{
        'value': document.getElementById('amount').value
      },
      'date':{
        'value': document.getElementById('date').value
      }
    }
  };
  try {
    const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'POST', param);


成功创建新记录后,调用函数以获取记录 ID 并上传文件。

// 成功
    console.log(resp);
    console.log(`Record ID:${resp.id}`);
    const rec_id = resp.id;
    console.log(`File:${file}`);
    if (file) {
      uploadFile(rec_id);
    }
    alert(`记录添加成功。记录ID: ${resp.id}`);


通过这个函数调用上传文件的 kintone API ,将下载的文件数据上传到 kintone

获取上传成功时返回的File Key值。
然后,调用更新记录的函数以便将记录 ID 与 File Key 相关联。

const uploadFile = (rec_id) => {

  let formData = new FormData();
  formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
  
  formData.append('file', file, file.name);

  const url = kintone.api.url('/k/v1/file.json', true);
  let xhr = new XMLHttpRequest();
  xhr.open('POST', url);
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  xhr.onload = () => {
    if (xhr.status === 200) {
      // 成功
      console.log(JSON.parse(xhr.responseText));
      const key = {'fileKey': JSON.parse(xhr.responseText).fileKey};
      updateRecord(rec_id, key);
    } else {
      // 错误
      console.log(JSON.parse(xhr.responseText));
    }
  };
  xhr.send(formData);
};


用于将记录 ID 与 File Key 关联的函数。

const updateRecord = async (rec_id, fileKey) => {
  const param = {
    'app': APP_ID,
    'id': rec_id,
    'record': {
      'receipt': {
        'value': [fileKey]
      }
    }
  }
  const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', param);
  // 成功
  console.log(resp);
};


参考网站

结束语

通常,经常使用的应用可以通过在 kintone 门户页面上显示并点击链接来使用。
但是,需要跳转到应用页面上才能使用。
通过 Kintone Portal Designer 进行自定义,可以在门户页面中,通过拖放来上传收据图像文件,比如经费报销应用,并将记录添加到相应的应用中,从而改善系统运用。

此Tips在2021年10月版的 kintone 中确认过。