mirror of
https://github.com/espressif/esp-idf.git
synced 2025-08-29 13:45:45 +00:00
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:
@@ -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">© 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 |
@@ -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>
|
||||
```
|
@@ -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,
|
||||
}
|
||||
}
|
@@ -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.
|
@@ -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 © {{ 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>
|
@@ -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')
|
||||
|
@@ -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.
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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.
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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',
|
||||
},
|
||||
})
|
||||
|
@@ -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
|
||||
}
|
||||
]
|
||||
})
|
@@ -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
|
@@ -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),
|
||||
}
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
@@ -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.
|
@@ -0,0 +1,8 @@
|
||||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
//
|
||||
}),
|
||||
})
|
@@ -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,
|
||||
}
|
||||
})
|
@@ -0,0 +1,4 @@
|
||||
// Utilities
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export default createPinia()
|
@@ -0,0 +1,3 @@
|
||||
# Styles
|
||||
|
||||
This directory is for configuring the styles of the application.
|
@@ -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
|
||||
// );
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
Reference in New Issue
Block a user