🎨 Node-RED Dashboard 2.0 実践ガイド【応用編】
第1部 template

このガイドでは、Dashboard 2.0の最も強力なウィジェットであるtemplateについて詳しく解説します。templateを使えば、Vue.jsVuetifyを活用してカスタムウィジェットを自由に作成できます。

📌 このガイドで学べること:

📑 目次

  1. 🚀 サンプルフロー
  2. templateとは?
  3. 4つのテンプレートタイプ
  4. Vue.js Options API
  5. 組み込み変数とメソッド
  6. Vuetifyコンポーネント
  7. 実用的な使用パターン
  8. 実践演習
  9. まとめ
  10. トラブルシューティング
  11. 実務での活用例
  12. 追加リソース

🚀 まずサンプルフローをインポートしましょう!

📥 まずサンプルフローをインポートしましょう!

以下のサンプルフローには、このガイドで説明する全パターンの実例が含まれています。
先にインポートして、実際に動作を確認しながら読み進めると理解が深まります。

  1. 下のサンプルフローJSONをコピー
  2. Node-REDエディタで メニュー → 読み込み を選択
  3. JSONをペーストして「読み込み」をクリック
  4. デプロイしてダッシュボードで動作確認
📋 template サンプルフロー(クリックで展開)

参照元:FlowFuse Dashboard 2.0 公式ドキュメント

🎯 1. templateとは?

🤔 「template」って何?

templateは、Dashboard 2.0で完全にカスタマイズされたウィジェットを作成できる最も強力なノードです。 日常生活で例えると、「DIY工房」のようなものです。他のウィジェットが「既製品の家具」だとすれば、templateは「自分で設計図を描いて、好きな材料で好きな家具を作れる工房」です。

🔧 DIY工房に例えると:

高度 template ウィジェット

Vue.jsとVuetifyを使って、完全にカスタマイズされたダッシュボードコンポーネントを作成できます。HTML、CSS、JavaScriptを組み合わせて、標準ウィジェットでは実現できない機能を実装できます。

📊 templateでできること

🎨 カスタムウィジェット

標準ウィジェットにない独自のUIコンポーネントを作成

例: 星評価、カラーピッカー、 ドラッグ&ドロップなど

🎭 カスタムCSS

ダッシュボード全体や特定ページのスタイルをカスタマイズ

例: テーマカラー変更、 フォント変更、レイアウト調整

📊 高度なデータ表示

Chart.jsやD3.jsを使った高度なグラフ・可視化

例: カスタムチャート、 ヒートマップ、3Dグラフ

🔗 外部ライブラリ連携

外部JavaScriptライブラリを読み込んで活用

例: Leaflet地図、 Three.js 3D表示など

📋 2. 4つのテンプレートタイプ

templateには4つの異なるタイプがあり、それぞれ用途が異なります。

タイプ スコープ 用途 Group設定
Widget ウィジェット (UI) グループ内 カスタムウィジェットの作成 必須
CSS スコープCSS グループ内 特定グループのスタイル変更 必須
Page ページ (UI) ページ全体 ページ全体に表示するUI要素 ページ選択
Site サイト全体CSS 全ページ ダッシュボード全体のスタイル ui-base選択

Widget ウィジェット (UI) タイプ

最も一般的なタイプで、カスタムウィジェットを作成します。指定したグループ内に表示されます。

<template> <v-btn @click="onClick">カスタムボタン</v-btn> </template> <script> export default { methods: { onClick() { this.send({ payload: "クリックされました" }); } } } </script>

CSS スコープCSSタイプ

特定のグループやウィジェットのスタイルをカスタマイズします。<style>タグのみを記述します。

<style> /* グループ内のボタンの色を変更 */ .v-btn { background-color: #ff5722 !important; } /* 特定のクラスにスタイル適用 */ .my-custom-class { font-size: 18px; font-weight: bold; } </style>

Page ページ (UI) タイプ

ページ全体に表示されるUI要素を作成します。フッターやページ共通の要素に最適です。

<template> <div class="page-footer"> <p>© 2024 My Dashboard - ページ共通フッター</p> </div> </template> <style scoped> .page-footer { position: fixed; bottom: 0; width: 100%; background: #333; color: white; text-align: center; padding: 10px; } </style>

Site サイト全体CSSタイプ

ダッシュボード全体に適用されるCSSを定義します。テーマのカスタマイズに使用します。

<style> /* サイト全体のフォント変更 */ body { font-family: 'Noto Sans JP', sans-serif !important; } /* プライマリカラーの変更 */ :root { --v-primary-base: #b92d5d !important; } /* ナビゲーションバーのスタイル */ .v-app-bar { background: linear-gradient(90deg, #b92d5d, #c00000) !important; } </style>

🔹 ノード設定プロパティ

プロパティ 説明 デフォルト
Group 表示するグループ(Widget/CSSタイプ時) -
Page 対象ページ(Pageタイプ時) -
UI 対象UI Base(Siteタイプ時) -
Name ノードの名前
Scope テンプレートスコープ(local/page/site) local
Order グループ内の表示順序 0
Size (Width / Height) ウィジェットの幅と高さ 0 (自動)
Template テンプレート本体(Vue.js SFC形式) デフォルトテンプレート
Pass Through 入力メッセージを出力にパススルー true
Class CSSクラス名

⚡ 3. Vue.js Options API

templateでは、Vue.jsのOptions APIを使用してコンポーネントを構築します。以下のプロパティがサポートされています。

プロパティ 説明 使用例
data コンポーネント内で使用するデータを定義する関数 カウンター、状態管理
methods テンプレートやスクリプトから呼び出せる関数を定義 ボタンクリック処理
computed 他の変数から計算される値を定義 合計値、フォーマット済み文字列
watch 変数の変更を監視して処理を実行 msgの変更検知
mounted コンポーネントがロードされた時に実行 初期化処理
unmounted コンポーネントが削除される時に実行 クリーンアップ処理

💡 Vue.js Options APIの基本構造:

<template> <!-- HTMLテンプレート(Vue.jsディレクティブ使用可能)--> <div>{{ message }}</div> </template> <script> export default { data() { return { message: "Hello World", count: 0 }; }, computed: { doubleCount() { return this.count * 2; } }, methods: { increment() { this.count++; this.send({ payload: this.count }); } }, watch: { msg(newMsg) { // msgが変更された時の処理 this.message = newMsg.payload; } }, mounted() { // コンポーネントロード時の初期化 console.log("コンポーネントがマウントされました"); } } </script>

🔧 4. 組み込み変数とメソッド

📦 組み込み変数

変数 説明 使用例
msg Node-REDから受信した最新のメッセージ {{ msg.payload }}
this.$socket Socket.IO接続オブジェクト カスタムイベントリスナー
this.$route 現在のルート情報(URLパラメータなど) this.$route.query.param
this.id ウィジェットのユニークID ソケットイベント識別

📤 組み込みメソッド

メソッド 説明 使用例
this.send(msg) Node-REDフローにメッセージを送信 this.send({ payload: "データ" })
this.submit() フォームデータをNode-REDに送信 フォーム送信処理

📬 メッセージ受信の2つの方法

方法1: watchでmsgを監視

watch: { msg(newMsg) { // 新しいメッセージを受信した時 // 注意: 初回ロード時も呼び出される this.data = newMsg.payload; } }

方法2: Socket.IOイベントリスナー

mounted() { // 新しいメッセージ受信時のみ実行(初回ロード時は実行されない) this.$socket.on('msg-input:' + this.id, (msg) => { this.data = msg.payload; }); }, unmounted() { // クリーンアップ this.$socket.off('msg-input:' + this.id); }

⚠️ watchとSocket.IOの違い:

用途に応じて使い分けましょう。

🎨 5. Vuetifyコンポーネント

templateでは、Vuetifyコンポーネントライブラリが標準で利用可能です。豊富な既製コンポーネントを使って、プロフェッショナルなUIを簡単に構築できます。

🔧 よく使うVuetifyコンポーネント

v-btn(ボタン)

<v-btn color="primary" @click="onClick"> クリック </v-btn>

v-text-field(テキスト入力)

<v-text-field v-model="inputText" label="入力してください" />

v-select(セレクトボックス)

<v-select v-model="selected" :items="options" label="選択" />

v-slider(スライダー)

<v-slider v-model="value" :min="0" :max="100" label="値" />

v-card(カード)

<v-card> <v-card-title>タイトル</v-card-title> <v-card-text>内容</v-card-text> </v-card>

v-file-input(ファイル入力)

<v-file-input v-model="file" label="ファイル選択" @change="onFileChange" />

💡 Vuetifyコンポーネント一覧:

全コンポーネントは Vuetify公式ドキュメント で確認できます。

🎯 7. 実用的な使用パターン

パターン1: 基本的なカスタムボタン

用途: シンプルなカスタムボタンでNode-REDにメッセージを送信

template
カスタムボタン
Debug
<template> <v-btn color="primary" size="large" @click="onClick"> 送信 </v-btn> </template> <script> export default { methods: { onClick() { this.send({ payload: "ボタンがクリックされました" }); } } } </script>

パターン2: データ受信・表示ウィジェット

用途: Node-REDからのメッセージを受信して表示

Inject template
データ表示
<template> <v-card> <v-card-title>受信データ</v-card-title> <v-card-text> <p v-if="msg">{{ msg.payload }}</p> <p v-else>データ待機中...</p> </v-card-text> </v-card> </template>

パターン3: 双方向バインディング(入力フォーム)

用途: ユーザー入力を受け取り、Node-REDに送信

template
入力フォーム
Function Debug
<template> <div> <v-text-field v-model="name" label="お名前" /> <v-text-field v-model="email" label="メールアドレス" type="email" /> <v-btn color="primary" @click="submit">送信</v-btn> </div> </template> <script> export default { data() { return { name: '', email: '' }; }, methods: { submit() { this.send({ payload: { name: this.name, email: this.email } }); } } } </script>

パターン4: 条件付きレンダリング(v-if/v-for)

用途: 状態に応じた動的な表示切り替え

<template> <div> <!-- 条件分岐 --> <v-alert v-if="status === 'success'" type="success">成功しました</v-alert> <v-alert v-else-if="status === 'error'" type="error">エラーが発生しました</v-alert> <v-alert v-else type="info">処理中...</v-alert> <!-- ループ表示 --> <v-list> <v-list-item v-for="(item, index) in items" :key="index"> {{ item.name }}: {{ item.value }} </v-list-item> </v-list> </div> </template> <script> export default { data() { return { status: 'pending', items: [] }; }, watch: { msg(newMsg) { this.status = newMsg.status; this.items = newMsg.payload || []; } } } </script>

パターン5: 外部ライブラリ(Chart.js)の使用

用途: Chart.jsを使ったカスタムグラフ

Inject
データ
template
Chart.js
<template> <canvas ref="chart"></canvas> </template> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> export default { data() { return { chart: null }; }, mounted() { this.initChart(); }, methods: { initChart() { const ctx = this.$refs.chart.getContext('2d'); this.chart = new Chart(ctx, { type: 'line', data: { labels: ['1月', '2月', '3月', '4月', '5月'], datasets: [{ label: 'データ', data: [12, 19, 3, 5, 2], borderColor: '#b92d5d' }] } }); } }, watch: { msg(newMsg) { if (this.chart && newMsg.payload) { this.chart.data.datasets[0].data = newMsg.payload; this.chart.update(); } } } } </script>

🏋️ 8. 実践演習

演習1: シンプルなトグルスイッチ初級

📝 課題:

Vuetifyのv-switchを使って、ON/OFFを切り替えるカスタムウィジェットを作成してください。

🎯 要求仕様:

📊 期待される出力:

💡 ヒント

ui-templateの設定:

  • タイプ: ウィジェット (UI)
  • グループ: 任意のグループを選択または作成

Vue.jsのポイント:

  • v-modelでスイッチの状態をデータにバインド
  • @changeまたはwatchで状態変更を検知
  • this.send()でメッセージ送信

コード例:

<v-switch v-model="isOn" label="電源" @change="onChange" />
✅ 解答例フロー
[ { "id": "ex1_tab", "type": "tab", "label": "演習1: トグルスイッチ", "disabled": false, "info": "" }, { "id": "ex1_template", "type": "ui-template", "z": "ex1_tab", "group": "ex1_group", "page": "", "ui": "", "name": "トグルスイッチ", "order": 1, "width": 0, "height": 0, "format": "<template>\n
\n \n

現在の状態: {{ isOn ? '動作中' : '停止中' }}

\n
\n</template>\n\n<script>\nexport default {\n data() {\n return {\n isOn: false\n };\n },\n methods: {\n onChange() {\n this.send({ payload: this.isOn });\n }\n },\n watch: {\n msg(newMsg) {\n if (typeof newMsg.payload === 'boolean') {\n this.isOn = newMsg.payload;\n }\n }\n }\n}\n</script>\n\n<style scoped>\n.toggle-widget {\n padding: 20px;\n text-align: center;\n}\n</style>", "passthru": true, "templateScope": "local", "className": "", "x": 200, "y": 100, "wires": [ [ "ex1_debug" ] ] }, { "id": "ex1_debug", "type": "debug", "z": "ex1_tab", "name": "出力確認", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 410, "y": 100, "wires": [] }, { "id": "ex1_group", "type": "ui-group", "name": "演習1", "page": "ex1_page", "width": "6", "height": "-1", "order": 1, "showTitle": true, "groupType": "default", "className": "", "visible": true, "disabled": false }, { "id": "ex1_page", "type": "ui-page", "name": "演習ページ", "ui": "ex1_base", "path": "/exercise", "icon": "mdi-school", "layout": "grid", "theme": "ex1_theme", "breakpoints": [ {"name": "Default", "px": 0, "cols": 3}, {"name": "Tablet", "px": 576, "cols": 6}, {"name": "Desktop", "px": 1024, "cols": 12} ], "order": 1, "className": "", "visible": true, "disabled": false }, { "id": "ex1_base", "type": "ui-base", "name": "Dashboard", "path": "/dashboard", "appIcon": "", "includeClientData": true, "acceptsClientConfig": ["ui-notification", "ui-control"], "showPathInSidebar": false, "headerContent": "page", "navigationStyle": "default", "titleBarStyle": "default", "showReconnectNotification": true, "notificationDisplayTime": 1, "showDisconnectNotification": true, "allowInstall": false }, { "id": "ex1_theme", "type": "ui-theme", "name": "Theme", "colors": { "surface": "#ffffff", "primary": "#b92d5d", "bgPage": "#eeeeee", "groupBg": "#ffffff", "groupOutline": "#cccccc" }, "sizes": { "pagePadding": "12px", "groupGap": "12px", "groupBorderRadius": "4px", "widgetGap": "12px", "density": "default" } } ]

演習2: リアルタイムセンサー表示中級

📝 課題:

センサーデータ(温度・湿度)を受信して、カード形式でリアルタイム表示するウィジェットを作成してください。

🎯 要求仕様:

📊 入力データ形式:

msg.payload = { "temperature": 25.5, "humidity": 60 }
💡 ヒント

Injectノードの設定:

  • msg.payload: JSON形式で温度・湿度を設定
  • 繰り返し: 5秒ごとに自動送信(テスト用)

Vue.jsのポイント:

  • computedで温度に応じた色を計算
  • v-cardとv-iconでカード表示
  • watchでmsg変更を監視

Vuetifyアイコン例:

温度: mdi-thermometer 湿度: mdi-water-percent
✅ 解答例フロー
[ { "id": "ex2_tab", "type": "tab", "label": "演習2: センサー表示", "disabled": false, "info": "" }, { "id": "ex2_inject", "type": "inject", "z": "ex2_tab", "name": "センサーデータ", "props": [ { "p": "payload" } ], "repeat": "5", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "{\"temperature\": 25.5, \"humidity\": 60}", "payloadType": "json", "x": 170, "y": 100, "wires": [ [ "ex2_random", "ex2_template" ] ] }, { "id": "ex2_random", "type": "function", "z": "ex2_tab", "name": "ランダム値生成", "func": "msg.payload = {\n temperature: Math.round((15 + Math.random() * 20) * 10) / 10,\n humidity: Math.round(40 + Math.random() * 40)\n};\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 360, "y": 100, "wires": [ [ "ex2_template" ] ] }, { "id": "ex2_template", "type": "ui-template", "z": "ex2_tab", "group": "ex2_group", "page": "", "ui": "", "name": "センサー表示", "order": 1, "width": 0, "height": 0, "format": "<template>\n
\n \n \n \n \n mdi-thermometer\n 温度\n \n \n {{ temperature }}°C\n \n \n \n \n \n \n mdi-water-percent\n 湿度\n \n \n {{ humidity }}%\n \n \n \n \n

最終更新: {{ lastUpdate }}

\n
\n</template>\n\n<script>\nexport default {\n data() {\n return {\n temperature: '--',\n humidity: '--',\n lastUpdate: '--'\n };\n },\n computed: {\n tempColor() {\n if (this.temperature === '--') return 'grey';\n if (this.temperature >= 30) return 'red';\n if (this.temperature <= 15) return 'blue';\n return 'green';\n }\n },\n watch: {\n msg(newMsg) {\n if (newMsg.payload) {\n this.temperature = newMsg.payload.temperature || '--';\n this.humidity = newMsg.payload.humidity || '--';\n this.lastUpdate = new Date().toLocaleTimeString('ja-JP');\n }\n }\n }\n}\n</script>\n\n<style scoped>\n.sensor-dashboard {\n padding: 10px;\n}\n.sensor-value {\n font-size: 36px;\n font-weight: bold;\n text-align: center;\n}\n.update-time {\n text-align: center;\n color: #666;\n margin-top: 15px;\n}\n</style>", "passthru": true, "templateScope": "local", "className": "", "x": 540, "y": 160, "wires": [ [] ] }, { "id": "ex2_group", "type": "ui-group", "name": "センサーモニター", "page": "ex2_page", "width": "6", "height": "-1", "order": 1, "showTitle": true, "groupType": "default", "className": "", "visible": true, "disabled": false }, { "id": "ex2_page", "type": "ui-page", "name": "センサー", "ui": "ex2_base", "path": "/sensor", "icon": "mdi-gauge", "layout": "grid", "theme": "ex2_theme", "breakpoints": [ {"name": "Default", "px": 0, "cols": 3}, {"name": "Tablet", "px": 576, "cols": 6}, {"name": "Desktop", "px": 1024, "cols": 12} ], "order": 1, "className": "", "visible": true, "disabled": false }, { "id": "ex2_base", "type": "ui-base", "name": "Dashboard", "path": "/dashboard", "appIcon": "", "includeClientData": true, "acceptsClientConfig": ["ui-notification", "ui-control"], "showPathInSidebar": false, "headerContent": "page", "navigationStyle": "default", "titleBarStyle": "default", "showReconnectNotification": true, "notificationDisplayTime": 1, "showDisconnectNotification": true, "allowInstall": false }, { "id": "ex2_theme", "type": "ui-theme", "name": "Theme", "colors": { "surface": "#ffffff", "primary": "#b92d5d", "bgPage": "#eeeeee", "groupBg": "#ffffff", "groupOutline": "#cccccc" }, "sizes": { "pagePadding": "12px", "groupGap": "12px", "groupBorderRadius": "4px", "widgetGap": "12px", "density": "default" } } ]

演習3: ToDoリスト中級

📝 課題:

タスクの追加・削除・完了切り替えができるToDoリストウィジェットを作成してください。

🎯 要求仕様:

💡 ヒント

データ構造:

todos: [ { id: 1, text: "タスク1", done: false }, { id: 2, text: "タスク2", done: true } ]

Vue.jsのポイント:

  • v-forでリスト表示
  • v-checkboxで完了状態管理
  • 配列操作: push(追加)、filter(削除)
✅ 解答例フロー
[ { "id": "ex3_tab", "type": "tab", "label": "演習3: ToDoリスト", "disabled": false, "info": "" }, { "id": "ex3_template", "type": "ui-template", "z": "ex3_tab", "group": "ex3_group", "page": "", "ui": "", "name": "ToDoリスト", "order": 1, "width": 0, "height": 0, "format": "<template>\n \n 📝 ToDoリスト\n \n \n \n \n <template v-slot:prepend>\n \n </template>\n \n {{ todo.text }}\n \n <template v-slot:append>\n \n </template>\n \n \n

完了: {{ completedCount }} / {{ todos.length }}

\n
\n
\n</template>\n\n<script>\nexport default {\n data() {\n return {\n newTask: '',\n todos: [],\n nextId: 1\n };\n },\n computed: {\n completedCount() {\n return this.todos.filter(t => t.done).length;\n }\n },\n methods: {\n addTask() {\n if (this.newTask.trim()) {\n this.todos.push({\n id: this.nextId++,\n text: this.newTask,\n done: false\n });\n this.newTask = '';\n this.updateTodos();\n }\n },\n removeTask(id) {\n this.todos = this.todos.filter(t => t.id !== id);\n this.updateTodos();\n },\n updateTodos() {\n this.send({ payload: this.todos });\n }\n },\n watch: {\n msg(newMsg) {\n if (Array.isArray(newMsg.payload)) {\n this.todos = newMsg.payload;\n this.nextId = Math.max(...this.todos.map(t => t.id), 0) + 1;\n }\n }\n }\n}\n</script>\n\n<style scoped>\n.todo-card {\n margin: 10px;\n}\n.done {\n text-decoration: line-through;\n color: #999;\n}\n.summary {\n text-align: center;\n margin-top: 15px;\n color: #666;\n}\n</style>", "passthru": true, "templateScope": "local", "className": "", "x": 200, "y": 100, "wires": [ [ "ex3_debug" ] ] }, { "id": "ex3_debug", "type": "debug", "z": "ex3_tab", "name": "ToDo出力", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 400, "y": 100, "wires": [] }, { "id": "ex3_group", "type": "ui-group", "name": "ToDo", "page": "ex3_page", "width": "6", "height": "-1", "order": 1, "showTitle": true, "groupType": "default", "className": "", "visible": true, "disabled": false }, { "id": "ex3_page", "type": "ui-page", "name": "ToDo", "ui": "ex3_base", "path": "/todo", "icon": "mdi-checkbox-marked", "layout": "grid", "theme": "ex3_theme", "breakpoints": [ {"name": "Default", "px": 0, "cols": 3}, {"name": "Tablet", "px": 576, "cols": 6}, {"name": "Desktop", "px": 1024, "cols": 12} ], "order": 1, "className": "", "visible": true, "disabled": false }, { "id": "ex3_base", "type": "ui-base", "name": "Dashboard", "path": "/dashboard", "appIcon": "", "includeClientData": true, "acceptsClientConfig": ["ui-notification", "ui-control"], "showPathInSidebar": false, "headerContent": "page", "navigationStyle": "default", "titleBarStyle": "default", "showReconnectNotification": true, "notificationDisplayTime": 1, "showDisconnectNotification": true, "allowInstall": false }, { "id": "ex3_theme", "type": "ui-theme", "name": "Theme", "colors": { "surface": "#ffffff", "primary": "#b92d5d", "bgPage": "#eeeeee", "groupBg": "#ffffff", "groupOutline": "#cccccc" }, "sizes": { "pagePadding": "12px", "groupGap": "12px", "groupBorderRadius": "4px", "widgetGap": "12px", "density": "default" } } ]

演習4: インタラクティブダッシュボード上級

📝 課題:

複数のデバイスの状態を表示し、各デバイスの制御ができるダッシュボードを作成してください。

🎯 要求仕様:

💡 ヒント

データ構造:

devices: [ { id: 'light', name: '照明', icon: 'mdi-lightbulb', on: false }, { id: 'aircon', name: 'エアコン', icon: 'mdi-air-conditioner', on: false }, { id: 'fan', name: '換気扇', icon: 'mdi-fan', on: false } ]

送信データ形式:

msg.payload = { device: 'light', command: 'on' // または 'off' }
✅ 解答例フロー
[ { "id": "ex4_tab", "type": "tab", "label": "演習4: デバイス制御", "disabled": false, "info": "" }, { "id": "ex4_template", "type": "ui-template", "z": "ex4_tab", "group": "ex4_group", "page": "", "ui": "", "name": "デバイス制御パネル", "order": 1, "width": 0, "height": 0, "format": "<template>\n
\n

🏠 スマートホーム制御

\n \n \n \n \n \n {{ device.icon }}\n \n

{{ device.name }}

\n \n {{ device.on ? 'ON' : 'OFF' }}\n \n
\n
\n
\n
\n \n
\n 全てON\n 全てOFF\n
\n
\n</template>\n\n<script>\nexport default {\n data() {\n return {\n devices: [\n { id: 'light', name: '照明', icon: 'mdi-lightbulb', on: false },\n { id: 'aircon', name: 'エアコン', icon: 'mdi-air-conditioner', on: false },\n { id: 'fan', name: '換気扇', icon: 'mdi-fan', on: false }\n ]\n };\n },\n methods: {\n toggleDevice(device) {\n device.on = !device.on;\n this.sendCommand(device.id, device.on ? 'on' : 'off');\n },\n allOn() {\n this.devices.forEach(d => {\n d.on = true;\n this.sendCommand(d.id, 'on');\n });\n },\n allOff() {\n this.devices.forEach(d => {\n d.on = false;\n this.sendCommand(d.id, 'off');\n });\n },\n sendCommand(deviceId, command) {\n this.send({\n payload: {\n device: deviceId,\n command: command\n },\n topic: 'device_control'\n });\n }\n },\n watch: {\n msg(newMsg) {\n if (newMsg.payload && newMsg.payload.device) {\n const device = this.devices.find(d => d.id === newMsg.payload.device);\n if (device) {\n device.on = newMsg.payload.command === 'on';\n }\n }\n }\n }\n}\n</script>\n\n<style scoped>\n.device-panel {\n padding: 20px;\n}\n.device-card {\n cursor: pointer;\n transition: transform 0.2s;\n}\n.device-card:hover {\n transform: scale(1.05);\n}\n.rotating {\n animation: spin 2s linear infinite;\n}\n@keyframes spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n.action-buttons {\n display: flex;\n gap: 10px;\n justify-content: center;\n}\n</style>", "passthru": true, "templateScope": "local", "className": "", "x": 220, "y": 100, "wires": [ [ "ex4_debug" ] ] }, { "id": "ex4_debug", "type": "debug", "z": "ex4_tab", "name": "制御コマンド", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload.device", "statusType": "auto", "x": 450, "y": 100, "wires": [] }, { "id": "ex4_group", "type": "ui-group", "name": "デバイス制御", "page": "ex4_page", "width": "6", "height": "-1", "order": 1, "showTitle": true, "groupType": "default", "className": "", "visible": true, "disabled": false }, { "id": "ex4_page", "type": "ui-page", "name": "スマートホーム", "ui": "ex4_base", "path": "/smart-home", "icon": "mdi-home-automation", "layout": "grid", "theme": "ex4_theme", "breakpoints": [ {"name": "Default", "px": 0, "cols": 3}, {"name": "Tablet", "px": 576, "cols": 6}, {"name": "Desktop", "px": 1024, "cols": 12} ], "order": 1, "className": "", "visible": true, "disabled": false }, { "id": "ex4_base", "type": "ui-base", "name": "Dashboard", "path": "/dashboard", "appIcon": "", "includeClientData": true, "acceptsClientConfig": ["ui-notification", "ui-control"], "showPathInSidebar": false, "headerContent": "page", "navigationStyle": "default", "titleBarStyle": "default", "showReconnectNotification": true, "notificationDisplayTime": 1, "showDisconnectNotification": true, "allowInstall": false }, { "id": "ex4_theme", "type": "ui-theme", "name": "Theme", "colors": { "surface": "#ffffff", "primary": "#b92d5d", "bgPage": "#eeeeee", "groupBg": "#ffffff", "groupOutline": "#cccccc" }, "sizes": { "pagePadding": "12px", "groupGap": "12px", "groupBorderRadius": "4px", "widgetGap": "12px", "density": "default" } } ]

🎓 9. まとめ

templateの重要ポイント

⚠️ よくある間違い

🔧 10. トラブルシューティング

よくある問題と解決方法

問題 原因 解決方法
ウィジェットが表示されない Group未設定またはエラー Groupを設定し、ブラウザのコンソールでエラー確認
メッセージが受信できない watch設定ミスまたはプロパティ名誤り watch: { msg(newMsg) {...} }の形式を確認
send()が動作しない thisの参照が失われている アロー関数を使用するか、外側でconst self = thisを保存
CSSが適用されない scopedの影響または優先度不足 !importantを追加またはscopedを外す
Vuetifyコンポーネントが動かない v-model設定ミスまたはデータ未定義 data()でv-modelに対応する変数を定義
ページリロードで状態がリセット 状態がブラウザ内のみ resendOnRefresh: trueを設定しNode-REDから再送信

💡 11. 実務での活用例

ケース1: カスタムセンサーダッシュボード

複数センサーからのデータ受信 ↓ ui-template(グラフ表示) - Chart.jsでリアルタイムグラフ - 閾値超過時に色変更 - 履歴データの表示切替 ↓ アラート表示 + データベース保存

ケース2: 設備制御パネル

ui-template(制御パネル) - 設備一覧をカード形式で表示 - 状態に応じたアイコン・色 - 制御ボタン(起動/停止/緊急停止) ↓ MQTT送信 → PLC/デバイス制御

ケース3: ファイルアップロードシステム

ui-template(ファイルアップロード) - v-file-inputでファイル選択 - プログレスバーで進捗表示 - ファイル情報のプレビュー ↓ Function(バッファ変換) ↓ Write File → ストレージ保存

ケース4: カスタムレポート画面

データベースクエリ ↓ ui-template(レポート表示) - v-data-tableで高度なテーブル - ソート・フィルター・ページネーション - CSVエクスポートボタン ↓ PDF生成 or メール送信

📚 12. 追加リソース


このガイドが役に立ちましたら、実際のプロジェクトで練習してみてください!
templateはDashboard 2.0の最も強力なウィジェットです。

参照元:FlowFuse Dashboard 2.0 公式ドキュメント

🏠