開発tax-assistant完了
Vue.jsで帳票画像ナビゲーションUIを共通コンポーネント化する
結論
複数のタブで重複していた「帳票画像」のナビゲーションUI(前へ/次へボタン、位置表示)を ImageNavigationPanel.vue という共通コンポーネントに切り出した。4つのタブすべてで同じUIが使われるようになり、保守性が向上した。
問題の背景
税務アシスタントアプリには複数のタブがあり、それぞれ帳票画像を表示するエリアを持っていた。
- 読取一覧タブ: ナビゲーションボタンと位置表示あり

- クレカ明細タブ: ナビゲーションがない状態

各タブが独自にナビゲーションUIを実装していたため、統一感がなく、同じコードが重複していた。
実装内容
1. 共通コンポーネントの作成
ImageNavigationPanel.vue を新規作成した。
<script setup lang="ts">
interface Props {
title?: string
currentIndex: number
totalCount: number
}
const props = withDefaults(defineProps<Props>(), {
title: '帳票画像',
})
const emit = defineEmits<{
prev: []
next: []
}>()
const positionDisplay = computed(() => {
if (props.totalCount === 0) return null
if (props.currentIndex < 0) return `- / ${props.totalCount}`
return `${props.currentIndex + 1} / ${props.totalCount}`
})
</script>
<template>
<div class="image-navigation-panel">
<div class="panel-title">
{{ title }}
<div class="nav-controls">
<button class="nav-btn" @click="emit('prev')">
前へ(←キー)
</button>
<button class="nav-btn" @click="emit('next')">
次へ(→キー)
</button>
<span v-if="positionDisplay" class="position-display">
{{ positionDisplay }}
</span>
</div>
</div>
<div class="image-container">
<slot />
</div>
</div>
</template>
ポイント:
- Nuxt 3の自動インポート:
computed等のVue APIはNuxtが自動インポートするため、明示的なimportは不要 - Props:
currentIndex(0始まり、未選択時は-1)とtotalCount(全体件数)を受け取る - Events:
prev/nextイベントを発火(ボタンの有効/無効制御は親コンポーネントで行う) - Slot: 画像コンテンツは親コンポーネントから注入
- 位置表示: 未選択時(
currentIndex < 0)は- / Nと表示 - キーボード操作: ボタンに「←キー」「→キー」と表示しているが、実際のキーボードイベント処理は親コンポーネント側で実装する設計
2. 各タブへの適用
MillerColumnsView(読取一覧)
<ImageNavigationPanel
:current-index="globalIndex"
:total-count="allItems.length"
@prev="goPrev"
@next="goNext"
>
<img v-if="selectedImageUrl" :src="selectedImageUrl" />
<div v-else class="no-data">レシートを選択</div>
</ImageNavigationPanel>
CreditCardMatchingView(クレカ明細)
<ImageNavigationPanel
:current-index="selectedIndex"
:total-count="filteredTransactions.length"
@prev="goPrev"
@next="goNext"
>
<div class="preview-content-inner">
<!-- 画像表示ロジック -->
</div>
</ImageNavigationPanel>
SquareMatchingView(Square明細)

ReceiptImage(科目別一覧で使用)

ReceiptImage.vue は内部で ImageNavigationPanel を使うように変更した。外部インターフェース(props, events)を維持したまま、内部実装だけを共通コンポーネントに委譲する形にした。
<template>
<div class="image-panel-wrapper">
<ImageNavigationPanel
:current-index="currentIndex ?? -1"
:total-count="totalCount ?? 0"
@prev="emit('prev')"
@next="emit('next')"
>
<img v-if="imageUrl" :src="imageUrl" />
<div v-else class="no-data">左のリストから項目を選択</div>
</ImageNavigationPanel>
</div>
</template>
統一後の状態
| タブ | コンポーネント | 使用方法 |
|---|---|---|
| 読取一覧 | ImageNavigationPanel | 直接使用 |
| 科目別一覧 | ReceiptImage | 内部で使用 |
| Square明細 | ImageNavigationPanel | 直接使用 |
| クレカ明細 | ImageNavigationPanel | 直接使用 |
すべてのタブで「帳票画像」「前へ(←キー)」「次へ(→キー)」「N / M」の統一されたUIになった。
コンポーネントAPI
| 種類 | 名前 | 型 | 説明 |
|---|---|---|---|
| Prop | title | string | タイトル(デフォルト: '帳票画像') |
| Prop | currentIndex | number | 現在位置(0始まり、未選択時は -1) |
| Prop | totalCount | number | 全体件数 |
| Event | prev | - | 「前へ」ボタンクリック時 |
| Event | next | - | 「次へ」ボタンクリック時 |
| Slot | default | - | 画像などのコンテンツ |
注意点
- ボタンのdisabled制御: コンポーネント内では行わない。親側で
currentIndexやtotalCountを見てイベントをガードする - 境界値:
currentIndex >= totalCountの場合は親側でクランプする想定 - キーボードイベント: ボタン表記に「←キー」「→キー」とあるが、実際のキーリスナーは各タブのコンポーネントで
window.addEventListener('keydown', ...)を登録する
まとめ
- 重複していたナビゲーションUIを1つの共通コンポーネントに集約
- Slotパターンを使い、画像以外のコンテンツ(複数画像表示など)にも対応
- 既存コンポーネント(
ReceiptImage)は外部インターフェースを維持しつつ内部実装を委譲 - 今後のUI変更は
ImageNavigationPanel.vueだけで対応できる