feat(restful_server): upgrade the example to use vue3+vuetify3

also cleaned up the backend firmware to use littlefs filesystem.
This commit is contained in:
morris
2025-07-17 23:26:12 +08:00
parent a5d53fc6a6
commit 70d62b1a54
55 changed files with 1263 additions and 597 deletions

View File

@@ -1,55 +1,9 @@
<template>
<v-app id="inspire">
<v-navigation-drawer v-model="drawer" fixed app clipped>
<v-list dense>
<v-list-tile to="/">
<v-list-tile-action>
<v-icon>home</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Home</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/chart">
<v-list-tile-action>
<v-icon>show_chart</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Chart</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/light">
<v-list-tile-action>
<v-icon>highlight</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Light</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar color="red accent-4" dark fixed app clipped-left>
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
<v-toolbar-title>ESP Home</v-toolbar-title>
</v-toolbar>
<v-content>
<v-container fluid fill-height>
<router-view></router-view>
</v-container>
</v-content>
<v-footer color="red accent-4" app fixed>
<span class="white--text">&copy; ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD. All rights reserved.</span>
</v-footer>
<v-app>
<router-view />
</v-app>
</template>
<script>
export default {
name: "App",
data() {
return {
drawer: null
};
}
};
<script setup>
//
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 223 KiB

View File

@@ -0,0 +1,35 @@
# Components
Vue template files in this folder are automatically imported.
## 🚀 Usage
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
The following example assumes a component located at `src/components/MyComponent.vue`:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
//
</script>
```
When your template is rendered, the component's import will automatically be inlined, which renders to this:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
import MyComponent from '@/components/MyComponent.vue'
</script>
```

View File

@@ -0,0 +1,141 @@
/**
* Composables for dashboard application
*/
import { onUnmounted, ref, watch } from 'vue'
import { lightApi, systemApi, tempApi } from '@/services/api'
/**
* Composable for system information
*/
export function useSystemInfo () {
const systemInfo = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchSystemInfo = async () => {
loading.value = true
error.value = null
try {
const response = await systemApi.getInfo().send()
systemInfo.value = response
} catch (error_) {
error.value = error_
console.error('Failed to fetch system info:', error_)
} finally {
loading.value = false
}
}
return {
systemInfo,
loading,
error,
fetchSystemInfo,
}
}
/**
* Composable for temperature data polling
*/
export function useTemperaturePolling (interval = 1000) {
const isPolling = ref(false)
const error = ref(null)
let timer = null
const startPolling = callback => {
if (isPolling.value) {
return
}
isPolling.value = true
timer = setInterval(async () => {
try {
// Force fresh request by bypassing cache
const response = await tempApi.getRaw().send({ force: true })
callback(response.raw)
error.value = null
} catch (error_) {
error.value = error_
console.error('Failed to fetch temperature data:', error_)
}
}, interval)
}
const stopPolling = () => {
if (timer) {
clearInterval(timer)
timer = null
}
isPolling.value = false
}
onUnmounted(() => {
stopPolling()
})
return {
isPolling,
error,
startPolling,
stopPolling,
}
}
/**
* Composable for light control
*/
export function useLightControl () {
// Load saved RGB values from localStorage or use defaults
const savedRed = localStorage.getItem('lightControl.red')
const savedGreen = localStorage.getItem('lightControl.green')
const savedBlue = localStorage.getItem('lightControl.blue')
const red = ref(savedRed ? Number.parseInt(savedRed) : 160)
const green = ref(savedGreen ? Number.parseInt(savedGreen) : 160)
const blue = ref(savedBlue ? Number.parseInt(savedBlue) : 160)
const loading = ref(false)
const error = ref(null)
// Watch for changes and save to localStorage
watch(red, newValue => {
localStorage.setItem('lightControl.red', newValue.toString())
})
watch(green, newValue => {
localStorage.setItem('lightControl.green', newValue.toString())
})
watch(blue, newValue => {
localStorage.setItem('lightControl.blue', newValue.toString())
})
const setColor = async () => {
loading.value = true
error.value = null
try {
// Ensure RGB values are integers (0-255)
const colorData = {
red: Math.round(Math.max(0, Math.min(255, red.value || 0))),
green: Math.round(Math.max(0, Math.min(255, green.value || 0))),
blue: Math.round(Math.max(0, Math.min(255, blue.value || 0))),
}
console.log('Setting color:', colorData)
const response = await lightApi.setBrightness(colorData).send()
console.log('Light control response:', response)
} catch (error_) {
error.value = error_
console.error('Failed to set color:', error_)
} finally {
loading.value = false
}
}
return {
red,
green,
blue,
loading,
error,
setColor,
}
}

View File

@@ -0,0 +1,5 @@
# Layouts
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts-next](https://github.com/loicduong/vite-plugin-vue-layouts-next) repository.

View File

@@ -0,0 +1,72 @@
<template>
<v-app>
<v-navigation-drawer v-model="drawer" app clipped>
<v-list density="compact">
<v-list-item
prepend-icon="$home"
title="Home"
to="/"
/>
<v-list-item
prepend-icon="$chart-line"
title="Chart"
to="/chart"
/>
<v-list-item
prepend-icon="$lightbulb"
title="Light"
to="/light"
/>
</v-list>
</v-navigation-drawer>
<v-app-bar app clipped-left color="blue-grey-darken-3">
<v-app-bar-nav-icon @click="drawer = !drawer" />
<v-app-bar-title>Dashboard</v-app-bar-title>
</v-app-bar>
<v-main>
<v-container fill-height fluid>
<router-view />
</v-container>
</v-main>
<v-footer app class="px-4 py-3" color="blue-grey-darken-4">
<v-row align="center" justify="space-between" no-gutters>
<v-col cols="auto">
<span class="text-grey-lighten-3 text-body-2">
Copyright &copy; {{ new Date().getFullYear() }} Espressif Systems. All rights reserved.
</span>
</v-col>
<v-col cols="auto">
<v-btn
class="mr-2"
color="grey-lighten-3"
href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/index.html"
:icon="mdiBookOpenPageVariant"
rel="noopener noreferrer"
size="small"
target="_blank"
variant="text"
/>
<v-btn
color="grey-lighten-3"
href="https://github.com/espressif/esp-idf"
:icon="mdiGithub"
rel="noopener noreferrer"
size="small"
target="_blank"
variant="text"
/>
</v-col>
</v-row>
</v-footer>
</v-app>
</template>
<script setup>
import { mdiBookOpenPageVariant, mdiGithub } from '@mdi/js'
import { ref } from 'vue'
const drawer = ref(null)
</script>

View File

@@ -1,16 +1,23 @@
import Vue from 'vue'
import './plugins/vuetify'
/**
* main.js
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Composables
import { createApp } from 'vue'
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
import router from './router'
import axios from 'axios'
import store from './store'
Vue.config.productionTip = false
// Styles
import 'unfonts.css'
Vue.prototype.$ajax = axios
const app = createApp(App)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
registerPlugins(app)
app.mount('#app')

View File

@@ -0,0 +1,5 @@
# Pages
Vue components created in this folder will automatically be converted to navigatable routes.
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.

View File

@@ -0,0 +1,85 @@
<template>
<v-container>
<v-row justify="center">
<v-col cols="12" lg="8">
<v-card class="pa-2">
<v-card-title class="d-flex align-center py-2">
<span class="text-h6">Temperature Chart</span>
<v-spacer />
<v-chip
:color="isPolling ? 'success' : 'error'"
size="small"
variant="flat"
>
{{ isPolling ? 'Live' : 'Offline' }}
</v-chip>
</v-card-title>
<v-card-text class="py-2">
<v-sparkline
auto-draw
:gradient="['#f72047', '#ffd200', '#1feaea']"
gradient-direction="top"
height="150"
:line-width="2"
:model-value="chartStore.chartValue"
:padding="4"
:smooth="10"
stroke-linecap="round"
/>
<v-alert
v-if="error"
class="mt-2"
density="compact"
dismissible
type="error"
@click:close="error = null"
>
Failed to fetch temperature data
</v-alert>
</v-card-text>
<v-card-actions class="py-2">
<v-btn
v-if="!isPolling"
color="primary"
size="default"
@click="startDataPolling"
>
<v-icon :icon="mdiPlay" start />
Start Monitoring
</v-btn>
<v-btn
v-else
color="error"
size="default"
@click="stopPolling"
>
<v-icon :icon="mdiStop" start />
Stop Monitoring
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { mdiPlay, mdiStop } from '@mdi/js'
import { onMounted } from 'vue'
import { useTemperaturePolling } from '@/composables/useApi'
import { useChartStore } from '@/stores/chart'
const chartStore = useChartStore()
const { isPolling, error, startPolling, stopPolling } = useTemperaturePolling(1000)
const startDataPolling = () => {
startPolling(newValue => {
chartStore.chartValue.push(newValue)
chartStore.chartValue.shift()
})
}
onMounted(() => {
startDataPolling()
})
</script>

View File

@@ -0,0 +1,59 @@
<template>
<v-container>
<v-row justify="center">
<v-col cols="12" sm="6">
<v-card :loading="loading">
<v-img
contain
height="200"
:src="logoSrc"
/>
<v-card-title class="justify-center">
<div class="text-center">
<div class="text-grey">
Chip: {{ systemInfo?.chip || 'Loading...' }}
</div>
<div class="text-grey">
CPU cores: {{ systemInfo?.cores || 'Loading...' }}
</div>
<div class="text-grey">
IDF version: {{ systemInfo?.idf_version || 'Loading...' }}
</div>
<v-alert
v-if="error"
class="mt-4"
dismissible
type="error"
@click:close="error = null"
>
Failed to load system information
</v-alert>
</div>
</v-card-title>
<v-card-actions class="justify-center">
<v-btn
color="primary"
:loading="loading"
@click="fetchSystemInfo"
>
Refresh
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import logoImage from '@/assets/logo.png'
import { useSystemInfo } from '@/composables/useApi'
const { systemInfo, loading, error, fetchSystemInfo } = useSystemInfo()
const logoSrc = logoImage
onMounted(() => {
fetchSystemInfo()
})
</script>

View File

@@ -0,0 +1,144 @@
<template>
<v-container>
<v-row justify="center">
<v-col cols="12" sm="6">
<v-card>
<v-responsive
class="d-flex align-center justify-center"
height="300"
:style="{ background: `rgb(${red}, ${green}, ${blue})` }"
>
<div class="text-center">
<h3 class="text-white text-shadow">
RGB({{ red }}, {{ green }}, {{ blue }})
</h3>
</div>
</v-responsive>
<v-card-text>
<v-container fluid>
<div class="mb-4">
<v-row align="center" no-gutters>
<v-col class="text-body-1 font-weight-medium" cols="2">
Red
</v-col>
<v-col class="px-3" cols="7">
<v-slider
v-model="red"
color="red"
hide-details
:max="255"
:min="0"
:step="1"
thumb-label
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="red"
density="compact"
hide-details
:max="255"
:min="0"
:step="1"
type="number"
/>
</v-col>
</v-row>
</div>
<div class="mb-4">
<v-row align="center" no-gutters>
<v-col class="text-body-1 font-weight-medium" cols="2">
Green
</v-col>
<v-col class="px-3" cols="7">
<v-slider
v-model="green"
color="green"
hide-details
:max="255"
:min="0"
:step="1"
thumb-label
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="green"
density="compact"
hide-details
:max="255"
:min="0"
:step="1"
type="number"
/>
</v-col>
</v-row>
</div>
<div class="mb-4">
<v-row align="center" no-gutters>
<v-col class="text-body-1 font-weight-medium" cols="2">
Blue
</v-col>
<v-col class="px-3" cols="7">
<v-slider
v-model="blue"
color="blue"
hide-details
:max="255"
:min="0"
:step="1"
thumb-label
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="blue"
density="compact"
hide-details
:max="255"
:min="0"
:step="1"
type="number"
/>
</v-col>
</v-row>
</div>
</v-container>
</v-card-text>
<v-card-actions class="justify-center">
<v-btn
color="red-accent-4"
:loading="loading"
prepend-icon="$check"
size="large"
@click="setColor"
>
Apply Color
</v-btn>
</v-card-actions>
<v-alert
v-if="error"
class="ma-4"
dismissible
type="error"
@click:close="error = null"
>
Failed to set light color
</v-alert>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { useLightControl } from '@/composables/useApi'
const { red, green, blue, loading, error, setColor } = useLightControl()
</script>
<style scoped>
.text-shadow {
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
}
</style>

View File

@@ -0,0 +1,3 @@
# Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.

View File

@@ -0,0 +1,10 @@
/**
* plugins/alova.js
*
* Alova HTTP client configuration
*/
import { alova } from '@/services/api'
export default function alovaPlugin (app) {
app.config.globalProperties.$alova = alova
}

View File

@@ -0,0 +1,19 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
import router from '@/router'
import pinia from '@/stores'
import alova from './alova'
// Plugins
import vuetify from './vuetify'
export function registerPlugins (app) {
app
.use(vuetify)
.use(router)
.use(pinia)
.use(alova)
}

View File

@@ -1,7 +1,45 @@
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com`
*/
Vue.use(Vuetify, {
iconfont: 'md',
import {
mdiChartLine,
mdiCheck,
mdiHome,
mdiLightbulb,
mdiMenu,
} from '@mdi/js'
// Composables
import { createVuetify } from 'vuetify'
// Icons
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
// Styles
import 'vuetify/styles'
// Custom icon aliases for on-demand loading
const customAliases = {
...aliases,
'home': mdiHome,
'chart-line': mdiChartLine,
'lightbulb': mdiLightbulb,
'check': mdiCheck,
'menu': mdiMenu,
}
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
icons: {
defaultSet: 'mdi',
aliases: customAliases,
sets: {
mdi,
},
},
theme: {
defaultTheme: 'light',
},
})

View File

@@ -1,29 +0,0 @@
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Chart from './views/Chart.vue'
import Light from './views/Light.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/chart',
name: 'chart',
component: Chart
},
{
path: '/light',
name: 'light',
component: Light
}
]
})

View File

@@ -0,0 +1,36 @@
/**
* router/index.ts
*
* Automatic routes for `./src/pages/*.vue`
*/
import { setupLayouts } from 'virtual:generated-layouts'
// Composables
import { createRouter, createWebHistory } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: setupLayouts(routes),
})
// Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
if (localStorage.getItem('vuetify:dynamic-reload')) {
console.error('Dynamic import error, reloading page did not fix it', err)
} else {
console.log('Reloading page to fix dynamic import error')
localStorage.setItem('vuetify:dynamic-reload', 'true')
location.assign(to.fullPath)
}
} else {
console.error(err)
}
})
router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
export default router

View File

@@ -0,0 +1,70 @@
/**
* API service using Alova
*
* CORS Solution: Use empty baseURL to leverage Vite proxy (no CORS issues)
*
* The Vite proxy in vite.config.mjs handles '/api' routes by forwarding them
* to the ESP32, avoiding CORS issues during development.
*/
import { createAlova } from 'alova'
import fetch from 'alova/fetch'
import VueHook from 'alova/vue'
export const alova = createAlova({
statesHook: VueHook,
requestAdapter: fetch(),
baseURL: '',
beforeRequest (method) {
// Minimize headers to avoid ESP32 431 error
const essentialHeaders = {}
// Only add Content-Type for POST requests
if (method.type === 'POST') {
essentialHeaders['Content-Type'] = 'application/json'
}
// Clear any existing headers and set only essential ones
method.config.headers = essentialHeaders
},
responded: {
onSuccess: async response => {
if (response.status >= 400) {
throw new Error(`HTTP Error: ${response.status}`)
}
// Check if response has content and is JSON
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
} else {
// For non-JSON responses (like light control), return text or empty object
const text = await response.text()
console.log('Non-JSON response:', text)
return text || { success: true }
}
},
onError: error => {
console.error('API Error:', error)
throw error
},
},
})
// API endpoints
export const systemApi = {
getInfo: () => alova.Get('/api/v1/system/info'),
}
export const tempApi = {
getRaw: () => {
// Create a fresh request each time with timestamp to prevent caching
return alova.Get(`/api/v1/temp/raw?_t=${Date.now()}`, {
localCache: 0, // Disable local cache
hitSource: 'network', // Always fetch from network
})
},
}
export const lightApi = {
setBrightness: data => alova.Post('/api/v1/light/brightness', data),
}

View File

@@ -1,28 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
chart_value: [8, 2, 5, 9, 5, 11, 3, 5, 10, 0, 1, 8, 2, 9, 0, 13, 10, 7, 16],
},
mutations: {
update_chart_value(state, new_value) {
state.chart_value.push(new_value);
state.chart_value.shift();
}
},
actions: {
update_chart_value({ commit }) {
axios.get("/api/v1/temp/raw")
.then(data => {
commit("update_chart_value", data.data.raw);
})
.catch(error => {
console.log(error);
});
}
}
})

View File

@@ -0,0 +1,5 @@
# Store
Pinia stores are used to store reactive state and expose actions to mutate it.
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.

View File

@@ -0,0 +1,8 @@
// Utilities
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
//
}),
})

View File

@@ -0,0 +1,25 @@
/**
* Chart data store using Pinia
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { tempApi } from '@/services/api'
export const useChartStore = defineStore('chart', () => {
const chartValue = ref([8, 2, 5, 9, 5, 11, 3, 5, 10, 0, 1, 8, 2, 9, 0, 13, 10, 7, 16])
const updateChartValue = async () => {
try {
const response = await tempApi.getRaw().send()
chartValue.value.push(response.raw)
chartValue.value.shift()
} catch (error) {
console.error('Failed to update chart value:', error)
}
}
return {
chartValue,
updateChartValue,
}
})

View File

@@ -0,0 +1,4 @@
// Utilities
import { createPinia } from 'pinia'
export default createPinia()

View File

@@ -0,0 +1,3 @@
# Styles
This directory is for configuring the styles of the application.

View File

@@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );

View File

@@ -1,41 +0,0 @@
<template>
<v-container fluid>
<v-sparkline
:value="get_chart_value"
:gradient="['#f72047', '#ffd200', '#1feaea']"
:smooth="10"
:padding="8"
:line-width="2"
stroke-linecap="round"
gradient-direction="top"
auto-draw
></v-sparkline>
</v-container>
</template>
<script>
export default {
data() {
return {
timer: null
};
},
computed: {
get_chart_value() {
return this.$store.state.chart_value;
}
},
methods: {
updateData: function() {
this.$store.dispatch("update_chart_value");
}
},
mounted() {
clearInterval(this.timer);
this.timer = setInterval(this.updateData, 1000);
},
destroyed: function() {
clearInterval(this.timer);
}
};
</script>

View File

@@ -1,40 +0,0 @@
<template>
<v-container>
<v-layout text-xs-center wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-img :src="require('../assets/logo.png')" contain height="200"></v-img>
<v-card-title primary-title>
<div class="ma-auto">
<span class="grey--text">IDF version: {{version}}</span>
<br>
<span class="grey--text">ESP cores: {{cores}}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return {
version: null,
cores: null
};
},
mounted() {
this.$ajax
.get("/api/v1/system/info")
.then(data => {
this.version = data.data.version;
this.cores = data.data.cores;
})
.catch(error => {
console.log(error);
});
}
};
</script>

View File

@@ -1,62 +0,0 @@
<template>
<v-container>
<v-layout text-xs-center wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-responsive :style="{ background: `rgb(${red}, ${green}, ${blue})` }" height="300px"></v-responsive>
<v-card-text>
<v-container fluid grid-list-lg>
<v-layout row wrap>
<v-flex xs9>
<v-slider v-model="red" :max="255" label="R"></v-slider>
</v-flex>
<v-flex xs3>
<v-text-field v-model="red" class="mt-0" type="number"></v-text-field>
</v-flex>
<v-flex xs9>
<v-slider v-model="green" :max="255" label="G"></v-slider>
</v-flex>
<v-flex xs3>
<v-text-field v-model="green" class="mt-0" type="number"></v-text-field>
</v-flex>
<v-flex xs9>
<v-slider v-model="blue" :max="255" label="B"></v-slider>
</v-flex>
<v-flex xs3>
<v-text-field v-model="blue" class="mt-0" type="number"></v-text-field>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
<v-btn fab dark large color="red accent-4" @click="set_color">
<v-icon dark>check_box</v-icon>
</v-btn>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return { red: 160, green: 160, blue: 160 };
},
methods: {
set_color: function() {
this.$ajax
.post("/api/v1/light/brightness", {
red: this.red,
green: this.green,
blue: this.blue
})
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
}
}
};
</script>