LoginSignup
14
2

More than 3 years have passed since last update.

kintone モバイルで QR バーコードを読み取るカスタマイズしてみた

Last updated at Posted at 2019-12-12

kintone Advent Calendar 2019 13日目の記事です。

はじめに

2019年、ついにバーコード決済デビューをしました。IC カード決済と組み合わせて、無敵な気持ちになっています:muscle:

なので、スマートフォン版の kintone で QRコードを読み取るカスタマイズをしてみました!
ポイントは、ネイティブのアプリを作らずに Web ブラウザ上で QR コードを読み取るところです。

このカスタマイズは、iOS Safari 限定で動作します。
動作確認は、iPhone XR(iOS 13.2.3)で行いました。

完成イメージ

  • レコード追加画面上部の[スキャン]ボタンをクリックすると、QRコードを読み取るモーダルが表示されます。
  • QRコードを読み取って[OK]ボタンを押すと、ルックアップで商品マスタから商品名を取得します。
  • あとは入出庫情報を入力して、レコードを保存します。

scan-barcode.gif

必要なもの

  • Node.js 10系 環境
  • kintone 環境(スタンダードコース)
    ※ 1年無料で利用できる開発者ライセンスがあります。

kintone アプリの作成

  • 入出庫履歴アプリ
    バーコード読み取りカスタマイズを行うアプリです。このアプリに JavaScript カスタマイズをします。

    フィールド名 フィールドタイプ フィールドコード 備考
    商品コード ルックアップ itemCode 商品マスタの商品コードに関連付ける
    商品名 文字列(1行) itemName 商品マスタの商品名からコピー
    種別 ラジオボタン 「入庫」「出庫」
    数量 数値 初期値: 0
  • 商品マスタ
    入出庫履歴に関連付けるアプリです。こちらはカスタマイズしません。
    あらかじめ、このアプリに商品レコードを追加しておきます。

    フィールド名 フィールドタイプ フィールドコード 備考
    コード 文字列(1行) itemCode 「値の重複を禁止する」にチェック
    商品名 文字列(1行) itemName

バーコードを読み取るライブラリの入手

2次元バーコードを読み取るライブラリには、InstaScan を利用します。
ただ、モバイルの Safari では動かないので、fork した InstaScan1を用意しました。

  1. fork した InstaScan にアクセスし、リポジトリをダウンロードします。
  2. リポジトリのディレクトリ以下で、次のコマンドを実行します。Node.js 10系で実行してください2

    # リポジトリのディレクトリに移動する
    $ cd instascan-master
    
    # ビルドに必要なツール群をインストールする
    $ npm install
    
    # ビルド
    $ npx gulp release 
    
  3. dist ディレクトリ以下に、「instascan.min.js」が生成されます。この後の「kintone カスタマイズの適用」で利用します。

kintone カスタマイズの適用

「入出庫履歴」アプリに、カスタマイズを適用します。

  • スマートフォン用の JavaScript ファイル(次の順で適用)

    1. https://js.cybozu.com/jquery/3.4.1/jquery.min.js
    2. instascan.min.js
    3. customize.js(カスタマイズファイル)
  • スマートフォン用の CSS ファイル

    1. customize.css(カスタマイズファイル)

カスタマイズファイル(customize.js customize.css )の詳細は次のとおりです。
Safari で動かすので、ES6+ の記法を使っています。

customize.js
(($) => {
  'use strict';
  let camera, result;

  /**
   *  スキャンボタンの HTML を生成する
   * @return {Object} scanButtonHTML
   */
  const getScanBtnHTML = () => {
    return `
      <div class="kpc-header">
        <button type="button" class="kpc-btn js-kcp-button"><i class="fas fa-barcode"></i>&nbsp;&nbsp;スキャン</button>
      </div>
    `;
  };

  /**
   * モーダルの HTML を生成する
   * @return {Object} modalHTML
   */
  const getModalHTML = () => {
    return `
      <div class="kcp-modal">
        <div class="kcp-modal__cointainer js-kcp-modal__cointainer">
          <div class="kpc-modal__header"><i class="fas fa-barcode"></i> QR コードをスキャン</div>
          <div class="kpc-video__container js-kpc-video__container"><video id="video" class="video"></video></div>
        <div class="kcp-modal__result">
          <dl>
            <dt>読み取り結果</dt>
            <dd>
              <input type="text" class="qrcode" name="itemCode" disabled>
            </dd>
          </dl>
        </div>
        <div class="js-kpc-modal__footer kpc-modal__footer">
          <div class="kpc-modal__footer--left"><button name="cancel"> キャンセル</button></div>
          <div class="kpc-modal__footer--right"><button name="ok" disabled> OK</button></div>
        </div>
        </div>
        <div class="kpc-modal__bg js-kpc-modal__bg"></div>
      </div>
    `;
  };

  /**
   * スキャナーオブジェクトを生成する
   * @param {jQuery} $modal
   * @return {Object} Scanner
   */
  const getScanner = ($modal) => {
    const $video = $modal.find('#video');
    const scanner = new Instascan.Scanner({video: $video[0], mirror: false});
    // 読み取れたときのイベントを登録
    scanner.addListener('scan', (_result) => {
      result = _result;
      $('.js-kpc-modal__footer button[name="ok"]').prop('disabled', false);
      $('input[name="itemCode"]').val(result);
    });
    return scanner;
  };

  /**
   * モーダルを開く
   */
  const openModal = () => {
    if ($('.kcp-modal').length > 0) {
      $('.kcp-modal').remove();
    }
    const $modal = $(getModalHTML());
    $('body').append($modal);
    $('input[name="itemCode"]').val('');

    const scanner = getScanner($modal);
    Instascan.Camera.getCameras().then((cameras) => {
      if (cameras.length <= 0) {
        console.error('No cameras found.');
        return;
      }
      camera = cameras[0];
      scanner.start(camera);
    }).catch((err) => {
      console.error(err);
    });

    $('.js-kpc-modal__footer button[name="ok"]').on('click', () => {
      const record = kintone.mobile.app.record.get();
      record.record.itemCode.value = result;
      record.record.itemCode.lookup = true;
      kintone.mobile.app.record.set(record);
      closeModal(scanner);
    });
    $('.js-kpc-modal__footer button[name="cancel"]').on('click', () => {
      closeModal(scanner);
    });
    $('.js-kpc-modal__bg,.js-kcp-modal__cointainer').fadeIn('slow');
  };

  /**
   * モーダルを閉じる
   * @param {Object} scanner スキャナー
   */
  const closeModal = (scanner) => {
    $('.js-kcp-modal__cointainer,.js-kpc-modal__bg').fadeOut('slow', () => {
      $('.js-kpc-modal__bg').remove();
      if (camera) {
        scanner.stop(camera);
      }
    });
  };

  // モバイル版のレコード追加画面を開いたときに実行する
  kintone.events.on('mobile.app.record.create.show', (event) => {
    const $scanBtn = $(getScanBtnHTML());
    $(kintone.mobile.app.getHeaderSpaceElement()).append($scanBtn);
    $scanBtn.find('.js-kcp-button').on('click', () => {
      openModal();
    });
    return event;
  });
})(jQuery);
customize.css
.kpc-header {
  padding: 10px;
}

.kpc-btn {
  padding: 8px 12px;
  font-size: 1em;
  font-weight: 700;
  line-height: 1;
  color: #fff;
  background-color: #206694;
  border: solid 2px #206694;
  border-radius: 6px;
}

.kpc-modal__bg {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1;
  display: none;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
}

.kcp-modal__cointainer {
  position: fixed;
  top: 2vh;
  right: 5vh;
  left: 5vh;
  z-index: 2;
  max-height: 95vh;
  background-color: #fff;
  border-radius: 6px 6px 6px 6px;
}

.kpc-modal__header {
  padding: 16px;
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1.4em;
  border-bottom: 1px solid #d8d8d8;
}

.kpc-video__container {
  position: relative;
  padding: 16px;
  line-height: 1.4em;
  border-bottom: 1px solid #d8d8d8;
}

.video {
  top: 0;
  z-index: 1;
  width: 100%;
}

.kcp-modal__result {
  position: relative;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  width: 100%;
  padding: 16px;
}

.kcp-modal__result dl {
  width: 100%;
}

.kcp-modal__result dt {
  margin-bottom: 5px;
  font-weight: 700;
}

.kcp-modal__result dd {
  width: 100%;
  margin-bottom: 5px;
}

.kpc-modal__footer {
  display: flex;
  padding: 10px 16px;
}

.kpc-modal__footer--left {
  display: flex;
  float: left;
  width: 50%;
}

.kpc-modal__footer--right {
  display: flex;
  float: right;
  width: 50%;
  text-align: right;
}

.kpc-modal__footer button {
  box-sizing: border-box;
  display: block;
  min-width: 100px;
  padding: 12px;
  font-weight: 700;
  line-height: 1;
  border: 2px solid #206694;
  border-radius: 6px;
}

.kpc-modal__footer button[name="ok"] {
  margin: 0 0 0 auto;
  color: #fff;
  background-color: #206694;
}

.kpc-modal__footer button[disabled] {
  cursor: default;
  background-color: #a5a5a5;
  border: 2px solid #a5a5a5;
}

.kpc-modal__footer button[name="cancel"] {
  color: #206694;
  background-color: #fff;
}

.kcp-modal__result input {
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  width: 100%;
  padding: 0.4em;
  border-radius: 0.4em;
  outline: 0;
}

.kcp-modal__result input[disabled] {
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  color: #999;
  -webkit-text-fill-color: #999;
  background-color: #d5d7d9;
  opacity: 1;
}

.qrcode {
  padding-right: 1.7em;
  margin-right: 0;
  vertical-align: middle;
}

おわりに

ネイティブアプリを作らなくても、少ないコード量の JavaScipt カスタマイズで、QR コードの読み取りができました!


  1. babelPolyfil の二重追加防止と、iOS Safari 対応をしています。 

  2. Node.js v10.16.3(npm 6.9.0)で実行できることを確認しました。 

14
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
2