Compare commits
10 Commits
f37e85a459
...
94105a040c
| Author | SHA1 | Date | |
|---|---|---|---|
|
94105a040c
|
|||
|
4c96815106
|
|||
|
3870eb644f
|
|||
|
238b310f84
|
|||
|
864d9dc0d0
|
|||
|
ec09713572
|
|||
|
970a38153e
|
|||
|
03e53aa389
|
|||
|
eecc2b354a
|
|||
|
543fbf575d
|
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = true
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.{vue,js,ts,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{rs,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[{justfile,*.just}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
@@ -3,9 +3,9 @@ application:
|
|||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
|
||||||
rate_limit:
|
rate_limit:
|
||||||
enabled: true
|
enabled: false
|
||||||
burst_size: 10
|
burst_size: 100
|
||||||
per_seconds: 60
|
per_seconds: 10
|
||||||
|
|
||||||
modbus:
|
modbus:
|
||||||
host: 192.168.1.200
|
host: 192.168.1.200
|
||||||
|
|||||||
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>STA</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-background text-text font-body">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'oxfmt';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
ignorePatterns: ['.direnv/**/*', '.gitea/**/*', 'backend/**/*', '**/*.toml', '**/*.md', '.sqlx/**/*'],
|
||||||
|
printWidth: 120,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
sortTailwindcss: true,
|
||||||
|
sortPackageJson: true,
|
||||||
|
allowParens: 'always',
|
||||||
|
jsdoc: true,
|
||||||
|
sortImports: true,
|
||||||
|
vueIndentScriptAndStyle: false,
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: ['typescript', 'unicorn', 'oxc', 'vue'],
|
||||||
|
categories: {
|
||||||
|
correctness: 'error',
|
||||||
|
},
|
||||||
|
rules: {},
|
||||||
|
env: {
|
||||||
|
builtin: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.direnv/**/*'],
|
||||||
|
});
|
||||||
+23
-9
@@ -1,25 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate:api": "curl -s http://localhost:3100/specs > openapi.yaml && openapi-typescript openapi.yaml -o src/api/schema.ts && rm openapi.yaml"
|
"generate:api": "curl -s http://localhost:3100/specs > openapi.yaml && openapi-typescript openapi.yaml -o src/api/schema.ts && rm openapi.yaml",
|
||||||
|
"lint": "oxlint",
|
||||||
|
"lint:fix": "oxlint --fix",
|
||||||
|
"fmt": "oxfmt",
|
||||||
|
"fmt:check": "oxfmt --check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"openapi-fetch": "^0.15.0",
|
"@primeuix/themes": "^2.0.3",
|
||||||
"vue": "^3.5.24"
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"openapi-fetch": "^0.15.2",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primevue": "^4.5.5",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"vue": "^3.5.34"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.12.4",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"openapi-typescript": "^7.10.1",
|
"less": "^4.6.4",
|
||||||
|
"less-loader": "^12.3.2",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
|
"oxfmt": "^0.49.0",
|
||||||
|
"oxlint": "^1.64.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.3.3",
|
||||||
"vue-tsc": "^3.1.4"
|
"vite-plugin-vue-devtools": "^8.1.2",
|
||||||
|
"vue-tsc": "^3.2.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2177
-404
File diff suppressed because it is too large
Load Diff
@@ -891,8 +891,8 @@ CLOSED: [2026-05-14 jeu. 20:09]
|
|||||||
- *File*: =src/presentation/api/relay_api.rs=
|
- *File*: =src/presentation/api/relay_api.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
|
|
||||||
*** TODO Frontend Implementation [0/2]
|
*** TODO Frontend Implementation [1/2]
|
||||||
- [ ] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
- [X] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
||||||
- Generate from OpenAPI spec or manually define
|
- Generate from OpenAPI spec or manually define
|
||||||
- *File*: =frontend/src/types/relay.ts=
|
- *File*: =frontend/src/types/relay.ts=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
|
|||||||
+11
-26
@@ -1,30 +1,15 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="min-h-screen flex flex-col">
|
||||||
<a href="https://vite.dev" target="_blank">
|
<StaHeader />
|
||||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
<main class="grow px-6 py-10 max-w-4xl mx-auto w-full">
|
||||||
</a>
|
<RelaysView />
|
||||||
<a href="https://vuejs.org/" target="_blank">
|
</main>
|
||||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
<StaFooter />
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<HelloWorld msg="Vite + Vue" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup lang="ts">
|
||||||
.logo {
|
import StaHeader from './components/StaHeader.vue';
|
||||||
height: 6em;
|
import StaFooter from './components/StaFooter.vue';
|
||||||
padding: 1.5em;
|
import RelaysView from './pages/RelaysView.vue';
|
||||||
will-change: filter;
|
</script>
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.vue:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #42b883aa);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+3
-8
@@ -13,17 +13,12 @@ To regenerate the TypeScript client after backend API changes:
|
|||||||
|
|
||||||
1. Start the backend server:
|
1. Start the backend server:
|
||||||
```bash
|
```bash
|
||||||
cargo run
|
just backend run
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Download the OpenAPI spec:
|
2. Execute the update script:
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3100/specs > openapi.yaml
|
just frontend sync
|
||||||
```
|
|
||||||
|
|
||||||
3. Generate TypeScript types:
|
|
||||||
```bash
|
|
||||||
pnpm exec openapi-typescript openapi.yaml -o src/api/schema.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|||||||
+4
-4
@@ -12,11 +12,11 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import createClient from 'openapi-fetch';
|
import createClient from "openapi-fetch";
|
||||||
import type { paths } from './schema';
|
import type { paths } from "./schema";
|
||||||
|
|
||||||
// Get the API base URL from environment variables or default to localhost
|
// Get the API base URL from environment variables or default to localhost
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3100';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3100";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typed API client instance.
|
* Typed API client instance.
|
||||||
@@ -28,4 +28,4 @@ export const apiClient = createClient<paths>({ baseUrl: API_BASE_URL });
|
|||||||
/**
|
/**
|
||||||
* Re-export the types for convenience
|
* Re-export the types for convenience
|
||||||
*/
|
*/
|
||||||
export type { paths, components } from './schema';
|
export type { paths, components } from "./schema";
|
||||||
|
|||||||
+97
-11
@@ -1,10 +1,7 @@
|
|||||||
/**
|
/** This file was auto-generated by openapi-typescript. Do not make direct changes to the file. */
|
||||||
* This file was auto-generated by openapi-typescript.
|
|
||||||
* Do not make direct changes to the file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
"/api/health": {
|
'/api/health': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -20,14 +17,14 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description Success */
|
/** Success */
|
||||||
200: {
|
200: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content?: never;
|
||||||
};
|
};
|
||||||
/** @description Too Many Requests - rate limit exceeded */
|
/** Too Many Requests - rate limit exceeded */
|
||||||
429: {
|
429: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -44,7 +41,7 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/meta": {
|
'/api/meta': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -60,16 +57,16 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description Success */
|
/** Success */
|
||||||
200: {
|
200: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json; charset=utf-8": components["schemas"]["Meta"];
|
'application/json; charset=utf-8': components['schemas']['Meta'];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Too Many Requests - rate limit exceeded */
|
/** Too Many Requests - rate limit exceeded */
|
||||||
429: {
|
429: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@@ -86,6 +83,76 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
'/api/relays': {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json; charset=utf-8': components['schemas']['RelayDto'][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
'/api/relays/{id}/toggle': {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json; charset=utf-8': components['schemas']['RelayDto'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
@@ -95,6 +162,25 @@ export interface components {
|
|||||||
version: string;
|
version: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* RelayDto
|
||||||
|
*
|
||||||
|
* Data Transfer Object for relay information. This struct represents a relay in a serialized format suitable for
|
||||||
|
* API responses. It contains the relay's ID, current state, and label in a format that can be easily serialized to
|
||||||
|
* JSON.
|
||||||
|
*/
|
||||||
|
RelayDto: {
|
||||||
|
/**
|
||||||
|
* Format: uint8
|
||||||
|
*
|
||||||
|
* The relay's unique identifier (1-8).
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/** The relay's current state as a string ("on" or "off"). */
|
||||||
|
state: string;
|
||||||
|
/** The relay's user-friendly label. */
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
parameters: never;
|
parameters: never;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,41 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps<{ msg: string }>()
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<button type="button" @click="count++">count is {{ count }}</button>
|
|
||||||
<p>
|
|
||||||
Edit
|
|
||||||
<code>components/HelloWorld.vue</code> to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Check out
|
|
||||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
|
||||||
>create-vue</a
|
|
||||||
>, the official Vue + Vite starter
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Learn more about IDE Support for Vue in the
|
|
||||||
<a
|
|
||||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
|
||||||
target="_blank"
|
|
||||||
>Vue Docs Scaling up Guide</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
'relay flex flex-col gap-10 bg-background-100 rounded-lg border-2 p-6 transition-all duration-300 ' +
|
||||||
|
relayClass
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row justify-between items-center">
|
||||||
|
<div class="flex flex-row gap-3 items-center">
|
||||||
|
<i class="pi pi-circle-fill"></i> <i class="pi pi-power-off"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge
|
||||||
|
:value="'Relay ' + props.relay.id"
|
||||||
|
:severity="isRelayOn ? 'primary' : 'secondary'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-between items-center">
|
||||||
|
<div>{{ props.relay.label }}</div>
|
||||||
|
<ToggleSwitch v-model="isRelayOn" v-on:click="toggleRelay(relay.id)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRelay } from '../composables/useRelay';
|
||||||
|
import { RelayState, type Relay } from '../types/relay';
|
||||||
|
import { Badge, ToggleSwitch } from 'primevue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
relay: Relay;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isRelayOn = computed(() => props.relay.state === RelayState.On);
|
||||||
|
|
||||||
|
const relayClass = computed(() => {
|
||||||
|
if (props.relay.state === RelayState.Off) {
|
||||||
|
return 'border-secondary shadow-md relay-off';
|
||||||
|
}
|
||||||
|
return 'border-primary shadow-lg shadow-primary-200 relay-on';
|
||||||
|
});
|
||||||
|
|
||||||
|
const { toggleRelay } = useRelay();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.relay {
|
||||||
|
width: 15rem;
|
||||||
|
&:hover {
|
||||||
|
scale: 1.02;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
&.pi-circle-fill {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
.relay-on & {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.relay-off & {
|
||||||
|
color: var(--color-secondary-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<footer
|
||||||
|
class="bg-background-200 border-t border-background-200 px-6 py-4 text-sm text-text"
|
||||||
|
>
|
||||||
|
<div class="max-w-4xl mx-auto text-center">
|
||||||
|
© {{ currentYear }} {{ appName }} ‐ Lucien Cartier-Tilet.
|
||||||
|
<a href="https://labs.phundrak.com/phundrak/sta"> Source code </a>
|
||||||
|
under the
|
||||||
|
<a
|
||||||
|
href="https://labs.phundrak.com/phundrak/sta/src/branch/develop/LICENSE.md"
|
||||||
|
>
|
||||||
|
AGPL 3.0 license </a
|
||||||
|
>.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useMeta } from '../composables/useMeta';
|
||||||
|
import { isNil } from '../utils/isNil';
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
const { metadata } = useMeta();
|
||||||
|
const appName = computed(() =>
|
||||||
|
isNil(metadata.value)
|
||||||
|
? 'STA'
|
||||||
|
: `${metadata.value.name} v${metadata.value.version}`,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped="scoped">
|
||||||
|
a {
|
||||||
|
color: var(--color-secondary-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
a {
|
||||||
|
@apply underline decoration-wavy underline-offset-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 bg-background-200 border-b border-background-200 shadow-sm px-6 py-4"
|
||||||
|
>
|
||||||
|
<nav class="flex items-center justify-between max-w-4xl mx-auto">
|
||||||
|
<span class="text-lg font-heading">
|
||||||
|
STA ‐ Smart Temperature & Appliance Control
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import type { components } from '../api/schema';
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
|
||||||
|
type Meta = components['schemas']['Meta'];
|
||||||
|
|
||||||
|
export function useMeta() {
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const metadata = ref<Meta | null>(null);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const getMetadata = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.GET('/api/meta');
|
||||||
|
error.value = null;
|
||||||
|
metadata.value = data as Meta;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to fetch metadata:', err);
|
||||||
|
error.value = err.message || 'Failed to fetch metadata';
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(getMetadata);
|
||||||
|
|
||||||
|
return { isLoading, metadata, error };
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
|
||||||
|
import type { Relay, RelayDto } from '../types/relay';
|
||||||
|
|
||||||
|
export function useRelay() {
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const response = ref<Relay | null>(null);
|
||||||
|
|
||||||
|
const toggleRelay = async (id: number) => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.POST('/api/relays/{id}/toggle', {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
error.value = null;
|
||||||
|
response.value = relayDtoToDomain(data as RelayDto);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`Failed to toggle relay ${id}:`, err);
|
||||||
|
error.value = err.message || `Failed to toggle relay ${id}`;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
toggleRelay,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
|
||||||
|
import type { Relay } from '../types/relay';
|
||||||
|
import { isNil } from '../utils/isNil';
|
||||||
|
|
||||||
|
export function useRelayPolling(intervalMs: number = 2000) {
|
||||||
|
const relays = ref<Relay[]>([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
let pollingInterval: number | null = null;
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.GET('/api/relays');
|
||||||
|
relays.value = data?.map(relayDtoToDomain) ?? [];
|
||||||
|
error.value = null;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Polling error:', err);
|
||||||
|
error.value = err.message || 'Failed to fetch data';
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPolling = () => {
|
||||||
|
fetchData();
|
||||||
|
pollingInterval = window.setInterval(fetchData, intervalMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (isNil(pollingInterval)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
pollingInterval = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(startPolling);
|
||||||
|
onUnmounted(stopPolling);
|
||||||
|
|
||||||
|
return {
|
||||||
|
relays,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refresh: fetchData,
|
||||||
|
};
|
||||||
|
}
|
||||||
+16
-4
@@ -1,5 +1,17 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue';
|
||||||
import './style.css'
|
import PrimeVue from 'primevue/config';
|
||||||
import App from './App.vue'
|
import Lara from '@primeuix/themes/lara';
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
import 'primeicons/primeicons.css';
|
||||||
|
import './style.css';
|
||||||
|
import './style.less';
|
||||||
|
import App from './App.vue';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(PrimeVue, {
|
||||||
|
theme: {
|
||||||
|
preset: Lara,
|
||||||
|
},
|
||||||
|
ripple: true,
|
||||||
|
});
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
<div v-if="isLoading && !relays">
|
||||||
|
<ProgressSpinner class="--p-progressspinner-color-primary" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="error"
|
||||||
|
class="bg-accent text-background py-4 px-3 rounded-md"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-row flex-wrap gap-4">
|
||||||
|
<RelayCard v-for="relay in relays" :relay="relay" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-wrap justify-evenly" style="display: none">
|
||||||
|
<Button severity="primary" class="min-w-2xs">Tout activer</Button>
|
||||||
|
<Button severity="secondary" class="min-w-2xs">Tout désactiver</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRelayPolling } from '../composables/useRelayPolling';
|
||||||
|
import { ProgressSpinner } from 'primevue';
|
||||||
|
import RelayCard from '../components/RelayCard.vue';
|
||||||
|
import { Button } from 'primevue';
|
||||||
|
|
||||||
|
const { relays, isLoading, error, refresh } = useRelayPolling();
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
+118
-71
@@ -1,79 +1,126 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css?family=Plus%20Jakarta%20Sans:700|Noto%20Sans:400');
|
||||||
|
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-jakarta: Plus Jakarta Sans, sans-serif;
|
||||||
|
--font-heading: Plus Jakarta Sans, sans-serif;
|
||||||
|
--font-noto: Noto Sans, sans-serif;
|
||||||
|
--font-body: Noto Sans, sans-serif;
|
||||||
|
--font-normal: 400;
|
||||||
|
--font-bold: 700;
|
||||||
|
--text-sm: 0.750rem;
|
||||||
|
--text-base: 1rem;
|
||||||
|
--text-xl: 1.333rem;
|
||||||
|
--text-2xl: 1.777rem;
|
||||||
|
--text-3xl: 2.369rem;
|
||||||
|
--text-4xl: 3.158rem;
|
||||||
|
--text-5xl: 4.210rem;
|
||||||
|
|
||||||
|
--color-text: oklch(20.55% 0.026 159.60);
|
||||||
|
--color-text-50: oklch(96.73% 0.012 164.80);
|
||||||
|
--color-text-100: oklch(93.53% 0.024 163.13);
|
||||||
|
--color-text-200: oklch(87.08% 0.048 162.29);
|
||||||
|
--color-text-300: oklch(80.85% 0.075 161.20);
|
||||||
|
--color-text-400: oklch(74.56% 0.099 159.20);
|
||||||
|
--color-text-500: oklch(68.48% 0.121 157.47);
|
||||||
|
--color-text-600: oklch(58.25% 0.101 157.47);
|
||||||
|
--color-text-700: oklch(47.56% 0.080 158.24);
|
||||||
|
--color-text-800: oklch(35.96% 0.056 158.77);
|
||||||
|
--color-text-900: oklch(23.61% 0.032 159.65);
|
||||||
|
--color-text-950: oklch(16.99% 0.020 157.52);
|
||||||
|
|
||||||
|
--color-background: oklch(98.85% 0.003 174.49);
|
||||||
|
--color-background-50: oklch(96.66% 0.009 179.60);
|
||||||
|
--color-background-100: oklch(93.48% 0.020 172.77);
|
||||||
|
--color-background-200: oklch(86.98% 0.039 173.82);
|
||||||
|
--color-background-300: oklch(80.46% 0.058 172.26);
|
||||||
|
--color-background-400: oklch(74.00% 0.077 170.71);
|
||||||
|
--color-background-500: oklch(67.67% 0.094 169.62);
|
||||||
|
--color-background-600: oklch(57.52% 0.079 169.17);
|
||||||
|
--color-background-700: oklch(46.93% 0.062 169.68);
|
||||||
|
--color-background-800: oklch(35.70% 0.045 170.66);
|
||||||
|
--color-background-900: oklch(23.47% 0.026 169.60);
|
||||||
|
--color-background-950: oklch(16.82% 0.014 169.51);
|
||||||
|
|
||||||
|
--color-primary: oklch(70.75% 0.113 157.63);
|
||||||
|
--color-primary-50: oklch(96.73% 0.012 164.80);
|
||||||
|
--color-primary-100: oklch(93.53% 0.024 163.13);
|
||||||
|
--color-primary-200: oklch(87.05% 0.049 161.02);
|
||||||
|
--color-primary-300: oklch(80.82% 0.076 160.38);
|
||||||
|
--color-primary-400: oklch(74.54% 0.100 158.60);
|
||||||
|
--color-primary-500: oklch(68.46% 0.122 157.00);
|
||||||
|
--color-primary-600: oklch(58.22% 0.102 156.89);
|
||||||
|
--color-primary-700: oklch(47.54% 0.081 157.46);
|
||||||
|
--color-primary-800: oklch(35.94% 0.057 157.56);
|
||||||
|
--color-primary-900: oklch(23.61% 0.032 159.65);
|
||||||
|
--color-primary-950: oklch(16.99% 0.020 157.52);
|
||||||
|
|
||||||
|
--color-secondary: oklch(77.49% 0.049 254.33);
|
||||||
|
--color-secondary-50: oklch(95.88% 0.009 247.92);
|
||||||
|
--color-secondary-100: oklch(91.80% 0.017 250.85);
|
||||||
|
--color-secondary-200: oklch(83.27% 0.035 253.73);
|
||||||
|
--color-secondary-300: oklch(74.79% 0.055 252.87);
|
||||||
|
--color-secondary-400: oklch(66.02% 0.075 253.94);
|
||||||
|
--color-secondary-500: oklch(57.42% 0.096 253.86);
|
||||||
|
--color-secondary-600: oklch(48.91% 0.081 254.25);
|
||||||
|
--color-secondary-700: oklch(40.26% 0.064 253.43);
|
||||||
|
--color-secondary-800: oklch(30.86% 0.044 254.23);
|
||||||
|
--color-secondary-900: oklch(20.97% 0.024 251.59);
|
||||||
|
--color-secondary-950: oklch(15.30% 0.015 257.65);
|
||||||
|
|
||||||
|
--color-accent: oklch(62.74% 0.101 280.46);
|
||||||
|
--color-accent-50: oklch(95.09% 0.012 281.08);
|
||||||
|
--color-accent-100: oklch(90.22% 0.024 283.36);
|
||||||
|
--color-accent-200: oklch(80.23% 0.051 282.68);
|
||||||
|
--color-accent-300: oklch(69.81% 0.082 281.67);
|
||||||
|
--color-accent-400: oklch(59.46% 0.112 280.05);
|
||||||
|
--color-accent-500: oklch(49.09% 0.144 277.36);
|
||||||
|
--color-accent-600: oklch(42.01% 0.120 277.54);
|
||||||
|
--color-accent-700: oklch(34.62% 0.096 277.83);
|
||||||
|
--color-accent-800: oklch(27.07% 0.066 278.62);
|
||||||
|
--color-accent-900: oklch(18.71% 0.036 279.84);
|
||||||
|
--color-accent-950: oklch(14.04% 0.022 283.20);
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
--p-button-primary-background: var(--color-primary) !important;
|
||||||
line-height: 1.5;
|
--p-button-primary-border-color: var(--color-primary) !important;
|
||||||
font-weight: 400;
|
--p-button-primary-hover-background: var(--color-primary-400) !important;
|
||||||
|
--p-button-primary-hover-border-color: var(--color-primary-400) !important;
|
||||||
|
--p-button-primary-active-background: var(--color-primary-300) !important;
|
||||||
|
--p-button-primary-active-border-color: var(--color-primary-300) !important;
|
||||||
|
--p-button-primary-color: var(--color-text) !important;
|
||||||
|
--p-button-primary-hover-color: var(--color-text) !important;
|
||||||
|
--p-button-primary-active-color: var(--color-text) !important;
|
||||||
|
|
||||||
color-scheme: light dark;
|
--p-button-secondary-background: var(--color-secondary) !important;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
--p-button-secondary-border-color: var(--color-secondary) !important;
|
||||||
background-color: #242424;
|
--p-button-secondary-hover-background: var(--color-secondary-400) !important;
|
||||||
|
--p-button-secondary-hover-border-color: var(--color-secondary-400) !important;
|
||||||
|
--p-button-secondary-active-background: var(--color-secondary-300) !important;
|
||||||
|
--p-button-secondary-active-border-color: var(--color-secondary-300) !important;
|
||||||
|
--p-button-secondary-color: var(--color-text) !important;
|
||||||
|
--p-button-secondary-hover-color: var(--color-text) !important;
|
||||||
|
--p-button-secondary-active-color: var(--color-text) !important;
|
||||||
|
|
||||||
font-synthesis: none;
|
--p-toggleswitch-border-color: var(--color-secondary-700) !important;
|
||||||
text-rendering: optimizeLegibility;
|
--p-toggleswitch-background: var(--color-secondary-50) !important;
|
||||||
-webkit-font-smoothing: antialiased;
|
--p-toggleswitch-handle-background: var(--color-secondary-700) !important;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
--p-toggleswitch-hover-border-color: var(--color-secondary-500) !important;
|
||||||
font-weight: 500;
|
--p-toggleswitch-hover-background: var(--color-secondary-50) !important;
|
||||||
color: #646cff;
|
--p-toggleswitch-handle-hover-background: var(--color-secondary-500) !important;
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
--p-toggleswitch-checked-background: var(--color-primary-400) !important;
|
||||||
margin: 0;
|
--p-toggleswitch-handle-checked-background: var(--color-primary-800) !important;
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
--p-toggleswitch-checked-hover-background: var(--color-primary-300) !important;
|
||||||
font-size: 3.2em;
|
--p-toggleswitch-handle-checked-hover-background: var(--color-primary-700) !important;
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
--p-badge-primary-background: var(--color-primary) !important;
|
||||||
border-radius: 8px;
|
--p-badge-primary-color: var(--color-text) !important;
|
||||||
border: 1px solid transparent;
|
--p-badge-secondary-background: var(--color-secondary-400) !important;
|
||||||
padding: 0.6em 1.2em;
|
--p-badge-secondary-color: var(--color-text) !important;
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { isNil } from '../../utils/isNil';
|
||||||
|
import { RelayState, Relay, type RelayDto } from '../relay';
|
||||||
|
|
||||||
|
const relayStateToDomain = (dto: string | null): RelayState => {
|
||||||
|
if (isNil(dto) || dto.trim() === '') {
|
||||||
|
return RelayState.Off;
|
||||||
|
}
|
||||||
|
return dto.trim().toLowerCase() === 'on' ? RelayState.On : RelayState.Off;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const relayDtoToDomain = (dto: RelayDto): Relay => {
|
||||||
|
return new Relay(dto.id, relayStateToDomain(dto.state), dto.label);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { components } from '../api/schema';
|
||||||
|
|
||||||
|
export type RelayDto = components['schemas']['RelayDto'];
|
||||||
|
|
||||||
|
export enum RelayState {
|
||||||
|
On = 'on',
|
||||||
|
Off = 'off',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Relay {
|
||||||
|
id: number;
|
||||||
|
state: RelayState;
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
constructor(id: number, state: RelayState, label: string) {
|
||||||
|
this.id = id;
|
||||||
|
this.state = state;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const isNil = (value: unknown | null | undefined): value is null | undefined =>
|
||||||
|
value === null || value === undefined;
|
||||||
+3
-2
@@ -8,9 +8,10 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{
|
||||||
{ "path": "./tsconfig.node.json" }
|
"path": "./tsconfig.app.json"
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exclude": [".direnv/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -18,9 +18,10 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -1,7 +1,16 @@
|
|||||||
import { defineConfig } from 'vite'
|
import * as path from 'path';
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||||
})
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user