create-yiougo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/index.js +76 -0
- package/package.json +45 -0
- package/template/.env.example +7 -0
- package/template/README.md +122 -0
- package/template/index.html +13 -0
- package/template/package-lock.json +3543 -0
- package/template/package.json +38 -0
- package/template/public/vite.svg +1 -0
- package/template/server/config/database.ts +31 -0
- package/template/server/config/redis.ts +23 -0
- package/template/server/index.ts +28 -0
- package/template/server/models/user.model.ts +10 -0
- package/template/server/routes/user.routes.ts +45 -0
- package/template/server/services/user.service.ts +100 -0
- package/template/shared/types/index.ts +14 -0
- package/template/src/App.vue +16 -0
- package/template/src/api/user.api.ts +53 -0
- package/template/src/components/BaseButton.vue +49 -0
- package/template/src/components/BaseCard.vue +41 -0
- package/template/src/components/index.ts +3 -0
- package/template/src/composables/index.ts +3 -0
- package/template/src/composables/useLoading.ts +36 -0
- package/template/src/composables/useNotification.ts +52 -0
- package/template/src/env.d.ts +13 -0
- package/template/src/hooks/index.ts +2 -0
- package/template/src/hooks/useApi.ts +60 -0
- package/template/src/layouts/default.vue +29 -0
- package/template/src/main.ts +20 -0
- package/template/src/pages/about.vue +111 -0
- package/template/src/pages/index.vue +38 -0
- package/template/src/pages/users.vue +126 -0
- package/template/src/router/index.ts +12 -0
- package/template/src/stores/user.store.ts +64 -0
- package/template/src/styles/main.scss +15 -0
- package/template/src/styles/quasar-variables.sass +8 -0
- package/template/src/types/global.d.ts +28 -0
- package/template/src/utils/date.ts +49 -0
- package/template/src/utils/index.ts +3 -0
- package/template/src/utils/storage.ts +56 -0
- package/template/src/vite-env.d.ts +7 -0
- package/template/tsconfig.app.json +32 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +23 -0
- package/template/tsconfig.server.json +29 -0
- package/template/vite.config.ts +42 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import { createPinia } from 'pinia';
|
|
3
|
+
import { Quasar } from 'quasar';
|
|
4
|
+
import router from './router';
|
|
5
|
+
import App from './App.vue';
|
|
6
|
+
|
|
7
|
+
// import layouts from 'virtual:generated-layouts' // vite-plugin-vue-layouts 自动处理
|
|
8
|
+
|
|
9
|
+
import '@quasar/extras/material-icons/material-icons.css';
|
|
10
|
+
import 'quasar/src/css/index.sass';
|
|
11
|
+
import './styles/main.scss';
|
|
12
|
+
|
|
13
|
+
const app = createApp(App);
|
|
14
|
+
const pinia = createPinia();
|
|
15
|
+
|
|
16
|
+
app.use(pinia);
|
|
17
|
+
app.use(router);
|
|
18
|
+
app.use(Quasar, {});
|
|
19
|
+
|
|
20
|
+
app.mount('#app');
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-page class="q-pa-md">
|
|
3
|
+
<q-card>
|
|
4
|
+
<q-card-section>
|
|
5
|
+
<div class="text-h4 q-mb-md">About Yiougo</div>
|
|
6
|
+
<p class="text-body1">
|
|
7
|
+
A modern full-stack application template built with cutting-edge technologies.
|
|
8
|
+
</p>
|
|
9
|
+
</q-card-section>
|
|
10
|
+
|
|
11
|
+
<q-separator />
|
|
12
|
+
|
|
13
|
+
<q-card-section>
|
|
14
|
+
<div class="text-h6 q-mb-md">Backend Stack</div>
|
|
15
|
+
<q-list>
|
|
16
|
+
<q-item>
|
|
17
|
+
<q-item-section avatar>
|
|
18
|
+
<q-icon color="primary" name="check_circle" />
|
|
19
|
+
</q-item-section>
|
|
20
|
+
<q-item-section>
|
|
21
|
+
<q-item-label>Elysia</q-item-label>
|
|
22
|
+
<q-item-label caption>Fast and ergonomic web framework for Bun</q-item-label>
|
|
23
|
+
</q-item-section>
|
|
24
|
+
</q-item>
|
|
25
|
+
<q-item>
|
|
26
|
+
<q-item-section avatar>
|
|
27
|
+
<q-icon color="primary" name="check_circle" />
|
|
28
|
+
</q-item-section>
|
|
29
|
+
<q-item-section>
|
|
30
|
+
<q-item-label>MongoDB</q-item-label>
|
|
31
|
+
<q-item-label caption>NoSQL database</q-item-label>
|
|
32
|
+
</q-item-section>
|
|
33
|
+
</q-item>
|
|
34
|
+
<q-item>
|
|
35
|
+
<q-item-section avatar>
|
|
36
|
+
<q-icon color="primary" name="check_circle" />
|
|
37
|
+
</q-item-section>
|
|
38
|
+
<q-item-section>
|
|
39
|
+
<q-item-label>Redis</q-item-label>
|
|
40
|
+
<q-item-label caption>In-memory data store for caching</q-item-label>
|
|
41
|
+
</q-item-section>
|
|
42
|
+
</q-item>
|
|
43
|
+
</q-list>
|
|
44
|
+
</q-card-section>
|
|
45
|
+
|
|
46
|
+
<q-separator />
|
|
47
|
+
|
|
48
|
+
<q-card-section>
|
|
49
|
+
<div class="text-h6 q-mb-md">Frontend Stack</div>
|
|
50
|
+
<q-list>
|
|
51
|
+
<q-item>
|
|
52
|
+
<q-item-section avatar>
|
|
53
|
+
<q-icon color="secondary" name="check_circle" />
|
|
54
|
+
</q-item-section>
|
|
55
|
+
<q-item-section>
|
|
56
|
+
<q-item-label>Vue 3</q-item-label>
|
|
57
|
+
<q-item-label caption>Progressive JavaScript framework</q-item-label>
|
|
58
|
+
</q-item-section>
|
|
59
|
+
</q-item>
|
|
60
|
+
<q-item>
|
|
61
|
+
<q-item-section avatar>
|
|
62
|
+
<q-icon color="secondary" name="check_circle" />
|
|
63
|
+
</q-item-section>
|
|
64
|
+
<q-item-section>
|
|
65
|
+
<q-item-label>Vite</q-item-label>
|
|
66
|
+
<q-item-label caption>Next generation frontend tooling</q-item-label>
|
|
67
|
+
</q-item-section>
|
|
68
|
+
</q-item>
|
|
69
|
+
<q-item>
|
|
70
|
+
<q-item-section avatar>
|
|
71
|
+
<q-icon color="secondary" name="check_circle" />
|
|
72
|
+
</q-item-section>
|
|
73
|
+
<q-item-section>
|
|
74
|
+
<q-item-label>Quasar</q-item-label>
|
|
75
|
+
<q-item-label caption>Vue component framework</q-item-label>
|
|
76
|
+
</q-item-section>
|
|
77
|
+
</q-item>
|
|
78
|
+
<q-item>
|
|
79
|
+
<q-item-section avatar>
|
|
80
|
+
<q-icon color="secondary" name="check_circle" />
|
|
81
|
+
</q-item-section>
|
|
82
|
+
<q-item-section>
|
|
83
|
+
<q-item-label>Pinia</q-item-label>
|
|
84
|
+
<q-item-label caption>Vue store</q-item-label>
|
|
85
|
+
</q-item-section>
|
|
86
|
+
</q-item>
|
|
87
|
+
<q-item>
|
|
88
|
+
<q-item-section avatar>
|
|
89
|
+
<q-icon color="secondary" name="check_circle" />
|
|
90
|
+
</q-item-section>
|
|
91
|
+
<q-item-section>
|
|
92
|
+
<q-item-label>Vue Router</q-item-label>
|
|
93
|
+
<q-item-label caption>Official router for Vue.js</q-item-label>
|
|
94
|
+
</q-item-section>
|
|
95
|
+
</q-item>
|
|
96
|
+
</q-list>
|
|
97
|
+
</q-card-section>
|
|
98
|
+
</q-card>
|
|
99
|
+
</q-page>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<route lang="yaml">
|
|
106
|
+
meta:
|
|
107
|
+
layout: default
|
|
108
|
+
</route>
|
|
109
|
+
|
|
110
|
+
<style lang="scss" scoped>
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-page class="flex flex-center">
|
|
3
|
+
<div class="text-center">
|
|
4
|
+
<h1 class="text-h2 text-primary q-mb-md">Welcome to Yiougo</h1>
|
|
5
|
+
<p class="text-h6 text-grey-7 q-mb-lg">
|
|
6
|
+
A modern full-stack application built with Elysia and Vue 3
|
|
7
|
+
</p>
|
|
8
|
+
<div class="q-gutter-md">
|
|
9
|
+
<q-btn
|
|
10
|
+
color="primary"
|
|
11
|
+
label="View Users"
|
|
12
|
+
icon="people"
|
|
13
|
+
to="/users"
|
|
14
|
+
size="lg"
|
|
15
|
+
/>
|
|
16
|
+
<q-btn
|
|
17
|
+
color="secondary"
|
|
18
|
+
label="About"
|
|
19
|
+
icon="info"
|
|
20
|
+
to="/about"
|
|
21
|
+
size="lg"
|
|
22
|
+
outline
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</q-page>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<route lang="yaml">
|
|
33
|
+
meta:
|
|
34
|
+
layout: default
|
|
35
|
+
</route>
|
|
36
|
+
|
|
37
|
+
<style lang="scss" scoped>
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-page class="q-pa-md">
|
|
3
|
+
<div class="row justify-between items-center q-mb-md">
|
|
4
|
+
<h4 class="q-ma-none">Users</h4>
|
|
5
|
+
<q-btn color="primary" label="Add User" icon="add" @click="showDialog = true" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<q-card>
|
|
9
|
+
<q-card-section>
|
|
10
|
+
<q-table
|
|
11
|
+
:rows="userStore.users"
|
|
12
|
+
:columns="columns"
|
|
13
|
+
row-key="_id"
|
|
14
|
+
:loading="userStore.loading"
|
|
15
|
+
flat
|
|
16
|
+
bordered
|
|
17
|
+
>
|
|
18
|
+
<template v-slot:body-cell-actions="props">
|
|
19
|
+
<q-td :props="props">
|
|
20
|
+
<q-btn
|
|
21
|
+
flat
|
|
22
|
+
round
|
|
23
|
+
dense
|
|
24
|
+
color="negative"
|
|
25
|
+
icon="delete"
|
|
26
|
+
@click="handleDelete(props.row._id)"
|
|
27
|
+
/>
|
|
28
|
+
</q-td>
|
|
29
|
+
</template>
|
|
30
|
+
</q-table>
|
|
31
|
+
</q-card-section>
|
|
32
|
+
</q-card>
|
|
33
|
+
|
|
34
|
+
<q-dialog v-model="showDialog">
|
|
35
|
+
<q-card style="min-width: 400px">
|
|
36
|
+
<q-card-section>
|
|
37
|
+
<div class="text-h6">Add New User</div>
|
|
38
|
+
</q-card-section>
|
|
39
|
+
|
|
40
|
+
<q-card-section class="q-pt-none">
|
|
41
|
+
<q-input v-model="newUser.username" label="Username" />
|
|
42
|
+
<q-input v-model="newUser.email" label="Email" type="email" />
|
|
43
|
+
<q-input v-model="newUser.password" label="Password" type="password" />
|
|
44
|
+
</q-card-section>
|
|
45
|
+
|
|
46
|
+
<q-card-actions align="right">
|
|
47
|
+
<q-btn flat label="Cancel" color="primary" v-close-popup />
|
|
48
|
+
<q-btn label="Create" color="primary" @click="handleCreate" />
|
|
49
|
+
</q-card-actions>
|
|
50
|
+
</q-card>
|
|
51
|
+
</q-dialog>
|
|
52
|
+
</q-page>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import { ref, onMounted } from 'vue';
|
|
57
|
+
import { useUserStore } from '@/stores/user.store';
|
|
58
|
+
import { useQuasar } from 'quasar';
|
|
59
|
+
|
|
60
|
+
const userStore = useUserStore();
|
|
61
|
+
const $q = useQuasar();
|
|
62
|
+
const showDialog = ref(false);
|
|
63
|
+
const newUser = ref({
|
|
64
|
+
username: '',
|
|
65
|
+
email: '',
|
|
66
|
+
password: ''
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const columns = [
|
|
70
|
+
{ name: 'username', label: 'Username', field: 'username', align: 'left' as const },
|
|
71
|
+
{ name: 'email', label: 'Email', field: 'email', align: 'left' as const },
|
|
72
|
+
{ name: 'createdAt', label: 'Created At', field: 'createdAt', align: 'left' as const },
|
|
73
|
+
{ name: 'actions', label: 'Actions', field: 'actions', align: 'center' as const }
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
onMounted(() => {
|
|
77
|
+
userStore.fetchUsers();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function handleCreate() {
|
|
81
|
+
await userStore.createUser(newUser.value);
|
|
82
|
+
if (!userStore.error) {
|
|
83
|
+
showDialog.value = false;
|
|
84
|
+
newUser.value = { username: '', email: '', password: '' };
|
|
85
|
+
$q.notify({
|
|
86
|
+
type: 'positive',
|
|
87
|
+
message: 'User created successfully'
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
$q.notify({
|
|
91
|
+
type: 'negative',
|
|
92
|
+
message: userStore.error
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleDelete(id: string) {
|
|
98
|
+
$q.dialog({
|
|
99
|
+
title: 'Confirm',
|
|
100
|
+
message: 'Are you sure you want to delete this user?',
|
|
101
|
+
cancel: true,
|
|
102
|
+
persistent: true
|
|
103
|
+
}).onOk(async () => {
|
|
104
|
+
await userStore.deleteUser(id);
|
|
105
|
+
if (!userStore.error) {
|
|
106
|
+
$q.notify({
|
|
107
|
+
type: 'positive',
|
|
108
|
+
message: 'User deleted successfully'
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
$q.notify({
|
|
112
|
+
type: 'negative',
|
|
113
|
+
message: userStore.error
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<route lang="yaml">
|
|
121
|
+
meta:
|
|
122
|
+
layout: default
|
|
123
|
+
</route>
|
|
124
|
+
|
|
125
|
+
<style lang="scss" scoped>
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import { setupLayouts } from 'virtual:generated-layouts'
|
|
3
|
+
import generatedRoutes from '~pages'
|
|
4
|
+
|
|
5
|
+
const routes = setupLayouts(generatedRoutes)
|
|
6
|
+
|
|
7
|
+
const router = createRouter({
|
|
8
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
9
|
+
routes,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export default router;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import type { User } from '@shared/types';
|
|
4
|
+
import { userApi } from '@/api/user.api';
|
|
5
|
+
|
|
6
|
+
export const useUserStore = defineStore('user', () => {
|
|
7
|
+
const users = ref<User[]>([]);
|
|
8
|
+
const loading = ref(false);
|
|
9
|
+
const error = ref<string | null>(null);
|
|
10
|
+
|
|
11
|
+
async function fetchUsers() {
|
|
12
|
+
loading.value = true;
|
|
13
|
+
error.value = null;
|
|
14
|
+
try {
|
|
15
|
+
const response = await userApi.getAll();
|
|
16
|
+
if (response.success) {
|
|
17
|
+
users.value = response.data;
|
|
18
|
+
}
|
|
19
|
+
} catch (err) {
|
|
20
|
+
error.value = err instanceof Error ? err.message : 'Failed to fetch users';
|
|
21
|
+
} finally {
|
|
22
|
+
loading.value = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function createUser(userData: Partial<User>) {
|
|
27
|
+
loading.value = true;
|
|
28
|
+
error.value = null;
|
|
29
|
+
try {
|
|
30
|
+
const response = await userApi.create(userData);
|
|
31
|
+
if (response.success) {
|
|
32
|
+
users.value.push(response.data);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
error.value = err instanceof Error ? err.message : 'Failed to create user';
|
|
36
|
+
} finally {
|
|
37
|
+
loading.value = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function deleteUser(id: string) {
|
|
42
|
+
loading.value = true;
|
|
43
|
+
error.value = null;
|
|
44
|
+
try {
|
|
45
|
+
const response = await userApi.delete(id);
|
|
46
|
+
if (response.success) {
|
|
47
|
+
users.value = users.value.filter(u => u._id !== id);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
error.value = err instanceof Error ? err.message : 'Failed to delete user';
|
|
51
|
+
} finally {
|
|
52
|
+
loading.value = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
users,
|
|
58
|
+
loading,
|
|
59
|
+
error,
|
|
60
|
+
fetchUsers,
|
|
61
|
+
createUser,
|
|
62
|
+
deleteUser
|
|
63
|
+
};
|
|
64
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 全局类型声明
|
|
2
|
+
declare module '~pages' {
|
|
3
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
4
|
+
const routes: RouteRecordRaw[]
|
|
5
|
+
export default routes
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module 'node:url' {
|
|
9
|
+
export * from 'url'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module 'vite-plugin-pages' {
|
|
13
|
+
export interface PagesOptions {
|
|
14
|
+
dirs?: string[]
|
|
15
|
+
extensions?: string[]
|
|
16
|
+
importMode?: 'sync' | 'async'
|
|
17
|
+
routeBlockLang?: string
|
|
18
|
+
}
|
|
19
|
+
export default function Pages(options?: PagesOptions): any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module 'vite-plugin-vue-layouts' {
|
|
23
|
+
export interface LayoutsOptions {
|
|
24
|
+
layoutsDirs?: string
|
|
25
|
+
defaultLayout?: string
|
|
26
|
+
}
|
|
27
|
+
export default function Layouts(options?: LayoutsOptions): any
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function formatDate(date: Date | string, format: string = 'YYYY-MM-DD'): string {
|
|
2
|
+
const d = new Date(date)
|
|
3
|
+
|
|
4
|
+
if (isNaN(d.getTime())) {
|
|
5
|
+
return ''
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const year = d.getFullYear()
|
|
9
|
+
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
10
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
11
|
+
const hours = String(d.getHours()).padStart(2, '0')
|
|
12
|
+
const minutes = String(d.getMinutes()).padStart(2, '0')
|
|
13
|
+
const seconds = String(d.getSeconds()).padStart(2, '0')
|
|
14
|
+
|
|
15
|
+
return format
|
|
16
|
+
.replace('YYYY', String(year))
|
|
17
|
+
.replace('MM', month)
|
|
18
|
+
.replace('DD', day)
|
|
19
|
+
.replace('HH', hours)
|
|
20
|
+
.replace('mm', minutes)
|
|
21
|
+
.replace('ss', seconds)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function timeAgo(date: Date | string): string {
|
|
25
|
+
const now = new Date()
|
|
26
|
+
const past = new Date(date)
|
|
27
|
+
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000)
|
|
28
|
+
|
|
29
|
+
if (diffInSeconds < 60) {
|
|
30
|
+
return '刚刚'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const diffInMinutes = Math.floor(diffInSeconds / 60)
|
|
34
|
+
if (diffInMinutes < 60) {
|
|
35
|
+
return `${diffInMinutes}分钟前`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const diffInHours = Math.floor(diffInMinutes / 60)
|
|
39
|
+
if (diffInHours < 24) {
|
|
40
|
+
return `${diffInHours}小时前`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const diffInDays = Math.floor(diffInHours / 24)
|
|
44
|
+
if (diffInDays < 30) {
|
|
45
|
+
return `${diffInDays}天前`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return formatDate(past)
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function getStorage<T>(key: string, defaultValue?: T): T | null {
|
|
2
|
+
try {
|
|
3
|
+
const item = localStorage.getItem(key)
|
|
4
|
+
if (item === null) {
|
|
5
|
+
return defaultValue ?? null
|
|
6
|
+
}
|
|
7
|
+
return JSON.parse(item) as T
|
|
8
|
+
} catch {
|
|
9
|
+
return defaultValue ?? null
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function setStorage<T>(key: string, value: T): void {
|
|
14
|
+
try {
|
|
15
|
+
localStorage.setItem(key, JSON.stringify(value))
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Failed to save to localStorage:', error)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeStorage(key: string): void {
|
|
22
|
+
try {
|
|
23
|
+
localStorage.removeItem(key)
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to remove from localStorage:', error)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clearStorage(): void {
|
|
30
|
+
try {
|
|
31
|
+
localStorage.clear()
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to clear localStorage:', error)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Session Storage
|
|
38
|
+
export function getSessionStorage<T>(key: string, defaultValue?: T): T | null {
|
|
39
|
+
try {
|
|
40
|
+
const item = sessionStorage.getItem(key)
|
|
41
|
+
if (item === null) {
|
|
42
|
+
return defaultValue ?? null
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(item) as T
|
|
45
|
+
} catch {
|
|
46
|
+
return defaultValue ?? null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function setSessionStorage<T>(key: string, value: T): void {
|
|
51
|
+
try {
|
|
52
|
+
sessionStorage.setItem(key, JSON.stringify(value))
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Failed to save to sessionStorage:', error)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true,
|
|
23
|
+
|
|
24
|
+
"baseUrl": ".",
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": ["./src/*"],
|
|
27
|
+
"@shared/*": ["./shared/*"]
|
|
28
|
+
},
|
|
29
|
+
"types": ["vite/client", "node"]
|
|
30
|
+
},
|
|
31
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "shared/**/*.ts"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
|
|
8
|
+
/* Bundler mode */
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
|
|
15
|
+
/* Linting */
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"noUncheckedSideEffectImports": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["vite.config.ts"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUnusedLocals": false,
|
|
17
|
+
"noUnusedParameters": false,
|
|
18
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
19
|
+
"esModuleInterop": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"baseUrl": ".",
|
|
23
|
+
"paths": {
|
|
24
|
+
"@shared/*": ["./shared/*"]
|
|
25
|
+
},
|
|
26
|
+
"types": ["bun-types"]
|
|
27
|
+
},
|
|
28
|
+
"include": ["server/**/*.ts", "shared/**/*.ts"]
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
2
|
+
|
|
3
|
+
import { defineConfig } from 'vite'
|
|
4
|
+
import vue from '@vitejs/plugin-vue'
|
|
5
|
+
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
|
6
|
+
import Pages from 'vite-plugin-pages'
|
|
7
|
+
import Layouts from 'vite-plugin-vue-layouts'
|
|
8
|
+
|
|
9
|
+
// https://vite.dev/config/
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [
|
|
12
|
+
vue({
|
|
13
|
+
template: { transformAssetUrls }
|
|
14
|
+
}),
|
|
15
|
+
Pages({
|
|
16
|
+
dirs: ['src/pages'],
|
|
17
|
+
extensions: ['vue'],
|
|
18
|
+
importMode: 'async',
|
|
19
|
+
routeBlockLang: 'yaml'
|
|
20
|
+
}),
|
|
21
|
+
Layouts(),
|
|
22
|
+
quasar({
|
|
23
|
+
sassVariables: fileURLToPath(new URL('./src/styles/quasar-variables.sass', import.meta.url))
|
|
24
|
+
})
|
|
25
|
+
],
|
|
26
|
+
resolve: {
|
|
27
|
+
alias: {
|
|
28
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
29
|
+
'@shared': fileURLToPath(new URL('./shared', import.meta.url))
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
server: {
|
|
33
|
+
port: 5173,
|
|
34
|
+
host: '0.0.0.0',
|
|
35
|
+
proxy: {
|
|
36
|
+
'/api': {
|
|
37
|
+
target: 'http://localhost:3000',
|
|
38
|
+
changeOrigin: true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|