■目的
勉強のためNuxt.jsのサンプル集を作る
目標5
GoogleBooksAPIを読書記録ウェブアプリを実装する
ざっくり仕様
- 検索はISBN13のみとする(本を探すではなく本を登録する、がメインのため)
- スマホ閲覧時、ISBNをスマホカメラで読み取る
- 検索した結果をコメント付きでローカルストレージに保存する
- コメントは再編集できるようにする
- ローカルストレージに保存したデータは削除できるようにする
- 保存したデータから何冊、何ページ読んだかわかるようにする
はじめに
これから書く内容を以下2記事がなければ絶対に実装できていなかった。
コードもまるっと拝借したりしている箇所もある。
こうやって記事をまとめてくれている人に感謝してもしきれない。
自分のまとめはあくまで自身の考えを整理するアウトプットなので、
実装方法を知りたい人は以下の参考記事を読むことをおすすめします。
default.vue
ベースファイルはここで書いたものと同じ。
components/template/Api.vue
内で処理をしていく。
※コードが長いので分割してまとめる
Api.vue(ISBN入力枠・抜粋)
<MoleculesEtcSearchIsbn v-model="isbn" />
SearchIsbn.vue
<template>
<input v-model="inputIsbn" type="text" placeholder="ISBN13">
</template>
<script>
export default {
props: {
value: {
type: String,
default: ''
}
},
computed: {
inputIsbn: {
get () {
return this.value
},
set (newValue) {
this.$emit('input', newValue)
}
}
}
}
</script>
アトミックデザインを意識した設計が目標なので、
コンポネント分けするのが大前提なのですが、
そのせいで、
・親で入力された値をpropsで渡して
・v-modelでリアクティブに
等など、考えることがたくさんあったISBN入力箇所。
子コンポネント側のcomputed
でprops
で受け取った値を
別の変数に格納して受け取った値を変更できるようにしています。
Api.vue(ISBN読み取りカメラ枠・抜粋)
<button class="p-api_box02--search-btn-scan" @click="startScan">
バーコードから探す<fa :icon="faCamera" />
</button>
<div v-show="camera" class="p-api_box02--search-scan" :style="scrollTopPosition">
<p class="p-api_box02--search-scan-txt">
「978」から始まるバーコードを映してください。
</p>
<div id="cameraArea" />
<button class="p-api_box02--search-btn-stop" aria-label="close" @click.prevent.stop="stopScan">
閉じる
</button>
</div>
バーコードのカメラ読み取りはQuagga.jsを採用。
@click="startScan"
でスマホのカメラが起動する。
スクリプトは後述。
Api.vue(ローディング中メッセージ表示枠・抜粋)
<template v-if="message">
<p class="p-api_box02--message">
{{ message }}
</p>
</template>
ちょっとしたUI。ISBNを感知して検索しているときに、「Now loading」と表示する枠。
v-if="message"
で判定する。
Api.vue(検索結果表示枠・抜粋)
<div v-for="item in items" :key="item.id" class="p-api_box02--result">
<MoleculesEtcSearchIsbnResult
v-model="itemComment"
:item-link="itemLink"
:item-img="itemImg"
:item-title="itemTitle"
:item-authors="itemAuthors"
:item-publisher="itemPublisher"
:item-description="item.volumeInfo.description"
:item-comment="itemComment"
@saveBtn="saveBtn"
/>
</div>
APIの検索結果から必要なものをitems
に格納して、そこから必要なものをprops
で
SearchIsbnResult
という子コンポーネントに渡す。
逆にlocalstrageに保存するためのボタンのイベントはemit
で子から渡してもらう。
SearchIsbnResult.vue
<template>
<div class="c-bookinfo">
<figure class="c-bookinfo_thumb">
<a :href="itemLink" target="_blank">
<img :src="itemImg">
</a>
</figure>
<dl class="c-bookinfo_data">
<dt class="c-bookinfo_data--title">
{{ itemTitle }}
</dt>
<dd class="c-bookinfo_data--detail">
<ul>
<li>
<dl>
<dt>作者</dt>
<dd>
<p v-for="(author, index) in itemAuthors" :key="index">
{{ author }}
</p>
</dd>
</dl>
</li>
<li>
<dl>
<dt>出版社</dt>
<dd>{{ itemPublisher }}</dd>
</dl>
</li>
</ul>
</dd>
</dl>
<p class="c-bookinfo_data--intro">
{{ itemDescription }}
</p>
<AtomsButtonsTextBtn
btn-style="outside"
color="dark"
:link-path="itemLink"
link-text="詳細を見る"
class="c-bookinfo_data--btn"
/>
<dl class="c-bookinfo_comment">
<dt>■コメント</dt>
<dd>
<textarea
v-model="inputItemComment"
/>
</dd>
</dl>
<p class="c-bookinfo_save" @click="saveBtn">
<span>保存する<fa :icon="faStickyNote" /></span>
</p>
</div>
</template>
<script>
import { faStickyNote } from '@fortawesome/free-solid-svg-icons'
export default {
props: {
itemLink: {
type: String,
default: ''
},
itemImg: {
type: String,
default: ''
},
itemTitle: {
type: String,
default: ''
},
itemAuthors: {
type: Array,
default: () => []
},
itemPublisher: {
type: String,
default: ''
},
itemDescription: {
type: String,
default: ''
},
itemComment: {
type: String,
default: ''
}
},
computed: {
faStickyNote () {
return faStickyNote
},
inputItemComment: {
get () {
return this.itemComment
},
set (newValue) {
this.$emit('input', newValue)
}
}
},
methods: {
saveBtn () {
this.$emit('saveBtn')
}
}
}
</script>
基本的に親から渡された値を表示しているだけ。
強いてポイントをあげると
- 「作者」は複数の場合があるので配列で処理
- 親から受け取った「コメント」を
get
set
で再処理
Api.vue(累計情報表示枠・抜粋)
<div v-if="booksLength > 0" class="p-api_box03--readedcount">
<p class="p-api_box03--readedcount-books">
あなたは<br>
<span>{{ booksLength }}</span>冊 / <span>{{ pagesCount }}</span>ページ<br>
読みました
</p>
</div>
localstrageに保存されているオブジェクトの数をカウントしたり、
その中のページ数を合計して計算したり。本読みにはちょっとした楽しい機能。
Api.vue(保存情報表示枠・抜粋)
<ul class="c-booklist">
<li v-for="(book, index) in books" :key="book.id">
<MoleculesEtcModalBook
v-model="modalItem.comment"
:book-img="book.img"
:book-img-modal="modalItem.img"
:book-link="modalItem.link"
:book-title-caption="book.title"
:book-title="modalItem.title"
:book-authors="modalItem.authors"
:book-publisher="modalItem.publisher"
:book-comment="modalItem.comment"
@open-modal="openModal(book, index)"
@delete-btn="deleteBtn()"
@resave-btn="resaveBtn()"
/>
</li>
</ul>
ここも基本的にprops
でデータを渡しているだけだけど、
- 配列の中からどの情報をモダルウィンドウで表示するか判定するために、引数を指定
- 削除や再保存ボタンを
$emit
で受け取る
とかこちょこちょやってる。
ModalBook.vue
<template>
<div class="c-booklist_wrap">
<figure class="c-booklist_thumb" @click="openModal">
<img :src="bookImg">
<figcaption>『{{ bookTitleCaption }}』</figcaption>
</figure>
<MoleculesEtcModal
v-if="isModalState"
:top-position="$window.pageYOffset"
@close-modal="$store.commit('Modal/closeModal')"
>
<figure class="c-booklist_thumb--modal">
<a :href="bookLink" target="_blank">
<img :src="bookImgModal">
</a>
</figure>
<dl class="c-booklist_data">
<dt class="c-booklist_data--title">
{{ bookTitle }}
</dt>
<dd>
<ul>
<li>
<dl>
<dt>作者</dt>
<dd>
<p v-for="(bookAuthor, index) in bookAuthors" :key="index">
{{ bookAuthor }}
</p>
</dd>
</dl>
</li>
<li>
<dl>
<dt>出版社</dt>
<dd>{{ bookPublisher }}</dd>
</dl>
</li>
</ul>
</dd>
</dl>
<dl class="c-booklist_comment">
<dt>■コメント</dt>
<dd class="c-booklist_comment-box">
<textarea ref="editComment" v-model="getBookComment" :class="(editFlag === true) ? 'is-editing' : ''" />
<template v-if="editFlag === false">
{{ bookComment }}
</template>
</dd>
<dd class="c-booklist_comment-edit">
<AtomsButtonsTextBtn
btn-style="bookEdit"
color="white"
link-text="再編集"
class="c-booklist_btn-edit"
@click.native="editComment"
/>
</dd>
<dd class="c-booklist_comment-resave">
<AtomsButtonsTextBtn
v-if="editFlag"
btn-style="bookResave"
color="white"
link-text="再保存する"
class="c-booklist_btn-resave"
@click.native="resaveComment"
/>
</dd>
</dl>
<AtomsButtonsTextBtn
btn-style="outside"
color="dark"
:link-path="bookLink"
link-text="詳細を見る"
class="c-booklist_btn"
/>
<AtomsButtonsTextBtn
btn-style="trash"
color="red"
link-text="本棚から削除する"
class="c-booklist_btn--trash"
@click.native="deleteBook"
/>
</MoleculesEtcModal>
</div>
</template>
<script>
export default {
// まずはpropsで必要なデータを受け取る
props: {
bookImg: {
type: String,
default: ''
},
bookImgModal: {
type: String,
default: ''
},
bookLink: {
type: null,
default: ''
},
bookTitle: {
type: String,
default: ''
},
bookTitleCaption: {
type: String,
default: ''
},
bookAuthors: {
type: Array,
default: () => []
},
bookPublisher: {
type: String,
default: ''
},
bookComment: {
type: String,
default: ''
},
countArray: {
type: String,
default: ''
}
},
data () {
return {
// コメント再編集状態を判定するためフラグを立てる
editFlag: false,
editedComment: ''
}
},
// モダルウィンドウ展開時、<body>にclass="modal-on"を付与
head () {
return {
bodyAttrs: {
class: this.isModalState ? 'modal-on' : ''
}
}
},
computed: {
// storeに書いたモダルウィンドウのフラグ(過去記事)
isModalState () {
return this.$store.state.Modal.modalFlag
},
// コメントが再編集されたら$emitする
getBookComment: {
get () {
return this.bookComment
},
set (newComment) {
this.$emit('input', newComment)
}
}
},
// $emitだったりフォーカスだったり
methods: {
openModal () {
this.$emit('open-modal')
},
deleteBook () {
this.$emit('delete-btn')
},
editComment () {
this.editFlag = !this.editFlag
this.$refs.editComment.focus()
},
resaveComment () {
this.$emit('resave-btn')
this.editFlag = false
}
}
}
</script>
ただの子コンポネント。データを表示したり、もらったり渡したり。
スクリプトはコメントの通り。
Api.vue(スクリプト部分)
これ以上コンパクトに書く方法がわからなくて長い。
コメントで解説。
<script>
import axios from 'axios'
import _ from 'lodash' // これを書かないとdebaunceの「_」でエラーが出た
import { faStickyNote, faCamera } from '@fortawesome/free-solid-svg-icons'
export default {
data () {
return {
// とにかく使うもの定義。絶対もっといい書き方がありそう
items: [],
isbn: '',
message: '',
books: [],
itemComment: '',
itemId: '',
itemTitle: '',
itemLink: [],
itemImg: '',
itemAuthors: [],
itemPublisher: '',
itemCount: 0,
modalItem: '',
Quagga: null,
code: '',
countArray: '',
camera: false,
scrollPos: 0,
readedLength: 0,
pageMath: []
}
},
computed: {
faStickyNote () {
return faStickyNote
},
faCamera () {
return faCamera
},
// スクロール量をscssで使う準備
scrollTopPosition () {
return {
'--top': this.$window.pageYOffset + 'px'
}
},
// localstrageに保存されているオブジェクトの数をカウント
booksLength () {
return this.books.length.toLocaleString()
},
// localstrageに保存されているページ数データの合計を取得
pagesCount () {
const booksCount = this.books.reduce(function (sum, element) {
return sum + element.count
}, 0).toLocaleString()
return booksCount
}
},
// 入力されたISBNコードをdebaunce対策を行って取得
watch: {
isbn (newIsbn, oldIsbn) {
this.message = 'Now loading...'
this.debouncedGetAnswer()
}
},
// localstrageにデータがあったらそれを引っ張ってきて、フラグを立てたり
// 削除でフラグを変えたり
mounted () {
if (localStorage.getItem('books')) {
try {
this.books = JSON.parse(localStorage.getItem('books'))
this.haveBooks = true
} catch (e) {
localStorage.removeItem('books')
this.haveBooks = false
}
}
},
// debaunce対策して1秒経ってから通信
created () {
this.debouncedGetAnswer = _.debounce(this.getAnswer, 1000)
},
methods: {
// ISBNを検知したらGoogleBooksAPIから情報を検索し、必要な変数に格納する
getAnswer () {
if (this.isbn) {
axios.get('https://www.googleapis.com/books/v1/volumes?q=isbn:' + this.isbn).then((response) => {
this.items = response.data.items
this.itemId = response.data.items[0].id
this.itemLink = response.data.items[0].volumeInfo.previewLink
this.itemTitle = response.data.items[0].volumeInfo.title
if (response.data.items[0].volumeInfo.imageLinks) {
this.itemImg = response.data.items[0].volumeInfo.imageLinks.thumbnail
} else {
this.itemImg = 'http://books.google.com/books/content?printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api'
}
this.itemAuthors = response.data.items[0].volumeInfo.authors
this.itemPublisher = response.data.items[0].volumeInfo.publisher
this.itemCount = response.data.items[0].volumeInfo.pageCount
})
}
this.message = ''
},
// localstrageに保存するデータをまとめたり、入力されている内容を削除したり
saveBtn () {
let saveGroup = {
id: this.itemId,
link: this.itemLink,
title: this.itemTitle,
img: this.itemImg,
authors: this.itemAuthors,
publisher: this.itemPublisher,
comment: this.itemComment.replace(/\\n/g, '\\n'),
count: this.itemCount
}
this.books.unshift(saveGroup)
saveGroup = ''
this.isbn = ''
this.items = ''
this.itemComment = ''
this.saveBook()
},
// localstrageに保存する
saveBook () {
const parsed = JSON.stringify(this.books)
localStorage.setItem('books', parsed)
},
// モダルウィンドウに渡すデータを判定
openModal (book, index) {
this.$store.commit('Modal/openModal')
this.modalItem = book
this.countArray = index
},
// localstrageから該当のオブジェクトを削除する
deleteBtn () {
this.books.splice(this.countArray, 1)
this.saveBook()
this.$store.commit('Modal/closeModal')
},
// コメントを再編集して保存する
resaveBtn () {
const activeBooks = this.countArray
this.books[activeBooks].comment = this.modalItem.comment
this.saveBook()
},
// 読み終わった本の数
readedBooks () {
this.readedLength = this.booksLength
},
// 読み終わった本の総ページ数
readedPages () {
this.pageMath = this.pagesCount
},
// ここからQuaagaJS
// 基本的に参考記事の通り(一部コメント)
startScan () {
this.code = ''
this.initQuagga()
this.camera = true
},
stopScan () {
this.Quagga.offProcessed(this.onProcessed)
this.Quagga.offDetected(this.onDetected)
this.Quagga.stop()
this.camera = false
},
initQuagga () {
this.Quagga = require('quagga')
this.Quagga.onProcessed(this.onProcessed)
this.Quagga.onDetected(this.onDetected)
// 設定
const config = {
inputStream: {
name: 'Live',
type: 'LiveStream',
target: document.querySelector('#cameraArea'),
constraints: {
decodeBarCodeRate: 3,
successTimeout: 500,
codeRepetition: true,
tryVertical: true,
frameRate: 15,
width: 640,
height: 480,
facingMode: 'environment'
},
area: {
top: '30%',
right: '0%',
bottom: '30%',
left: '0%'
}
},
locator: {
patchSize: 'medium',
halfSample: true
},
numOfWorkers: navigator.hardwareConcurrency || 4,
decoder: {
readers: ['ean_reader'],
multiple: false
},
locate: true
}
this.Quagga.init(config, this.onInit)
},
onInit (err) {
if (err) {
return
}
this.Quagga.start()
},
onDetected (success) {
this.code = success.codeResult.code
// ISBN13は978から始まるので、読み取った数字の最初3文字が978であれば
// 変数isbnに値を渡すよう限定的に処理
// こうしないと誤認識が多くて使えなかった
if (this.code.slice(0, 3) === '978') {
this.isbn = this.code
this.Quagga.stop()
this.camera = false
}
},
onProcessed (result) {
const drawingCtx = this.Quagga.canvas.ctx.overlay
const drawingCanvas = this.Quagga.canvas.dom.overlay
if (result) {
// 検出中の緑の線の枠
if (result.boxes) {
drawingCtx.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height)
const hasNotRead = box => box !== result.box
result.boxes.filter(hasNotRead).forEach((box) => {
this.Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
color: 'green',
lineWidth: 2
})
})
}
// 検出に成功した瞬間の青い線の枠
if (result.box) {
this.Quagga.ImageDebug.drawPath(
result.box,
{ x: 0, y: 1 },
drawingCtx,
{
color: 'blue',
lineWidth: 2
}
)
}
// 検出に成功した瞬間の水平の赤い線
if (result.codeResult && result.codeResult.code) {
this.Quagga.ImageDebug.drawPath(
result.line,
{ x: 'x', y: 'y' },
drawingCtx,
{
color: 'red',
lineWidth: 3
}
)
}
}
}
}
}
</script>
Api.vue(全文)
※QuaggaJSのcssなどは割愛
<template>
<main>
<section id="p-api_box01">
<div class="c-box">
<AtomsTitlesTit
tit-tag="h2"
tit-class=""
tit-txt="Api"
/>
<p class="f-txt">
GoogleBooksAPIでISBNに限定した検索を行い、その内容をローカルストレージに保存する。
</p>
</div>
</section><!-- /box01 -->
<section id="p-api_box02">
<div class="c-box">
<AtomsTitlesTit
tit-tag="h3"
tit-class="middle"
tit-txt="Search for ISBNs on Google Books"
/>
<dl class="p-api_box02--search">
<dt>ISBN検索</dt>
<dd>
<MoleculesEtcSearchIsbn v-model="isbn" />
<button class="p-api_box02--search-btn-scan" @click="startScan">
バーコードから探す<fa :icon="faCamera" />
</button>
<div v-show="camera" class="p-api_box02--search-scan" :style="scrollTopPosition">
<p class="p-api_box02--search-scan-txt">
「978」から始まるバーコードを映してください。
</p>
<div id="cameraArea" />
<button class="p-api_box02--search-btn-stop" aria-label="close" @click.prevent.stop="stopScan">
閉じる
</button>
</div>
</dd>
</dl>
<template v-if="message">
<p class="p-api_box02--message">
{{ message }}
</p>
</template>
<div v-for="item in items" :key="item.id" class="p-api_box02--result">
<MoleculesEtcSearchIsbnResult
v-model="itemComment"
:item-link="itemLink"
:item-img="itemImg"
:item-title="itemTitle"
:item-authors="itemAuthors"
:item-publisher="itemPublisher"
:item-description="item.volumeInfo.description"
:item-comment="itemComment"
@saveBtn="saveBtn"
/>
</div>
</div>
</section><!-- /box02 -->
<section v-if="booksLength > 0" id="p-api_box03">
<div class="c-box">
<AtomsTitlesTit
tit-tag="h3"
tit-class="middle"
tit-txt="Your Favorite Books"
/>
<div v-if="booksLength > 0" class="p-api_box03--readedcount">
<p class="p-api_box03--readedcount-books">
あなたは<br>
<span>{{ booksLength }}</span>冊 / <span>{{ pagesCount }}</span>ページ<br>
読みました
</p>
</div>
<ul class="c-booklist">
<li v-for="(book, index) in books" :key="book.id">
<MoleculesEtcModalBook
v-model="modalItem.comment"
:book-img="book.img"
:book-img-modal="modalItem.img"
:book-link="modalItem.link"
:book-title-caption="book.title"
:book-title="modalItem.title"
:book-authors="modalItem.authors"
:book-publisher="modalItem.publisher"
:book-comment="modalItem.comment"
@open-modal="openModal(book, index)"
@delete-btn="deleteBtn()"
@resave-btn="resaveBtn()"
/>
</li>
</ul>
</div>
</section><!-- /box03 -->
<AtomsButtonsTextBtn
btn-style="rounded back"
color="dark"
link-path=""
link-text="Back to Front"
/>
</main>
</template>
<script>
import axios from 'axios'
import _ from 'lodash'
import { faStickyNote, faCamera } from '@fortawesome/free-solid-svg-icons'
export default {
data () {
return {
items: [],
isbn: '',
message: '',
books: [],
itemComment: '',
itemId: '',
itemTitle: '',
itemLink: [],
itemImg: '',
itemAuthors: [],
itemPublisher: '',
itemCount: 0,
modalItem: '',
Quagga: null,
code: '',
countArray: '',
camera: false,
scrollPos: 0,
readedLength: 0,
pageMath: []
}
},
computed: {
faStickyNote () {
return faStickyNote
},
faCamera () {
return faCamera
},
scrollTopPosition () {
return {
'--top': this.$window.pageYOffset + 'px'
}
},
booksLength () {
return this.books.length.toLocaleString()
},
pagesCount () {
const booksCount = this.books.reduce(function (sum, element) {
return sum + element.count
}, 0).toLocaleString()
return booksCount
}
},
watch: {
isbn (newIsbn, oldIsbn) {
this.message = 'Now loading...'
this.debouncedGetAnswer()
}
},
mounted () {
if (localStorage.getItem('books')) {
try {
this.books = JSON.parse(localStorage.getItem('books'))
this.haveBooks = true
} catch (e) {
localStorage.removeItem('books')
this.haveBooks = false
}
}
},
created () {
this.debouncedGetAnswer = _.debounce(this.getAnswer, 1000)
},
methods: {
getAnswer () {
if (this.isbn) {
axios.get('https://www.googleapis.com/books/v1/volumes?q=isbn:' + this.isbn).then((response) => {
this.items = response.data.items
this.itemId = response.data.items[0].id
this.itemLink = response.data.items[0].volumeInfo.previewLink
this.itemTitle = response.data.items[0].volumeInfo.title
if (response.data.items[0].volumeInfo.imageLinks) {
this.itemImg = response.data.items[0].volumeInfo.imageLinks.thumbnail
} else {
this.itemImg = 'http://books.google.com/books/content?printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api'
}
this.itemAuthors = response.data.items[0].volumeInfo.authors
this.itemPublisher = response.data.items[0].volumeInfo.publisher
this.itemCount = response.data.items[0].volumeInfo.pageCount
})
}
this.message = ''
},
saveBtn () {
let saveGroup = {
id: this.itemId,
link: this.itemLink,
title: this.itemTitle,
img: this.itemImg,
authors: this.itemAuthors,
publisher: this.itemPublisher,
comment: this.itemComment.replace(/\\n/g, '\\n'),
count: this.itemCount
}
this.books.unshift(saveGroup)
saveGroup = ''
this.isbn = ''
this.items = ''
this.itemComment = ''
this.saveBook()
},
saveBook () {
const parsed = JSON.stringify(this.books)
localStorage.setItem('books', parsed)
},
openModal (book, index) {
this.$store.commit('Modal/openModal')
this.modalItem = book
this.countArray = index
},
deleteBtn () {
this.books.splice(this.countArray, 1)
this.saveBook()
this.$store.commit('Modal/closeModal')
},
resaveBtn () {
const activeBooks = this.countArray
this.books[activeBooks].comment = this.modalItem.comment
this.saveBook()
},
readedBooks () {
this.readedLength = this.booksLength
},
readedPages () {
this.pageMath = this.pagesCount
},
startScan () {
this.code = ''
this.initQuagga()
this.camera = true
},
stopScan () {
this.Quagga.offProcessed(this.onProcessed)
this.Quagga.offDetected(this.onDetected)
this.Quagga.stop()
this.camera = false
},
initQuagga () {
this.Quagga = require('quagga')
this.Quagga.onProcessed(this.onProcessed)
this.Quagga.onDetected(this.onDetected)
// 設定
const config = {
inputStream: {
name: 'Live',
type: 'LiveStream',
target: document.querySelector('#cameraArea'),
constraints: {
decodeBarCodeRate: 3,
successTimeout: 500,
codeRepetition: true,
tryVertical: true,
frameRate: 15,
width: 640,
height: 480,
facingMode: 'environment'
},
area: {
top: '30%',
right: '0%',
bottom: '30%',
left: '0%'
}
},
locator: {
patchSize: 'medium',
halfSample: true
},
numOfWorkers: navigator.hardwareConcurrency || 4,
decoder: {
readers: ['ean_reader'],
multiple: false
},
locate: true
}
this.Quagga.init(config, this.onInit)
},
onInit (err) {
if (err) {
return
}
this.Quagga.start()
},
onDetected (success) {
this.code = success.codeResult.code
if (this.code.slice(0, 3) === '978') {
this.isbn = this.code
this.Quagga.stop()
this.camera = false
}
},
onProcessed (result) {
const drawingCtx = this.Quagga.canvas.ctx.overlay
const drawingCanvas = this.Quagga.canvas.dom.overlay
if (result) {
// 検出中の緑の線の枠
if (result.boxes) {
drawingCtx.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height)
const hasNotRead = box => box !== result.box
result.boxes.filter(hasNotRead).forEach((box) => {
this.Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
color: 'green',
lineWidth: 2
})
})
}
// 検出に成功した瞬間の青い線の枠
if (result.box) {
this.Quagga.ImageDebug.drawPath(
result.box,
{ x: 0, y: 1 },
drawingCtx,
{
color: 'blue',
lineWidth: 2
}
)
}
// 検出に成功した瞬間の水平の赤い線
if (result.codeResult && result.codeResult.code) {
this.Quagga.ImageDebug.drawPath(
result.line,
{ x: 'x', y: 'y' },
drawingCtx,
{
color: 'red',
lineWidth: 3
}
)
}
}
}
}
}
</script>
結果
正直、バーコードの読み取り精度に難はあると思う。
でも求めていた機能を実装することができた。