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,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yiougo-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:server": "bun run --watch server/index.ts",
|
|
9
|
+
"build": "vue-tsc && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"server": "bun run server/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"elysia": "^1.1.25",
|
|
15
|
+
"@elysiajs/cors": "^1.1.0",
|
|
16
|
+
"@elysiajs/swagger": "^1.1.5",
|
|
17
|
+
"mongodb": "^6.11.0",
|
|
18
|
+
"redis": "^4.7.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"vue": "^3.5.13",
|
|
22
|
+
"vue-router": "^4.5.0",
|
|
23
|
+
"pinia": "^2.3.0",
|
|
24
|
+
"quasar": "^2.17.3",
|
|
25
|
+
"@quasar/extras": "^1.16.12",
|
|
26
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
27
|
+
"@quasar/vite-plugin": "^1.7.3",
|
|
28
|
+
"vite": "^6.0.7",
|
|
29
|
+
"vue-tsc": "^2.1.10",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"sass-embedded": "^1.81.0",
|
|
32
|
+
"@types/node": "^22.10.5",
|
|
33
|
+
"@types/bun": "latest",
|
|
34
|
+
"@elysiajs/eden": "^1.1.10",
|
|
35
|
+
"vite-plugin-vue-layouts": "^0.11.0",
|
|
36
|
+
"vite-plugin-pages": "^0.32.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MongoClient, Db } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
let db: Db;
|
|
4
|
+
let client: MongoClient;
|
|
5
|
+
|
|
6
|
+
export async function connectDB() {
|
|
7
|
+
try {
|
|
8
|
+
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/yiougo';
|
|
9
|
+
client = new MongoClient(uri);
|
|
10
|
+
await client.connect();
|
|
11
|
+
db = client.db();
|
|
12
|
+
console.log('✅ MongoDB connected successfully');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('❌ MongoDB connection error:', error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDB(): Db {
|
|
20
|
+
if (!db) {
|
|
21
|
+
throw new Error('Database not initialized. Call connectDB first.');
|
|
22
|
+
}
|
|
23
|
+
return db;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function closeDB() {
|
|
27
|
+
if (client) {
|
|
28
|
+
await client.close();
|
|
29
|
+
console.log('MongoDB connection closed');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createClient } from 'redis';
|
|
2
|
+
|
|
3
|
+
export const redisClient = createClient({
|
|
4
|
+
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export async function connectRedis() {
|
|
8
|
+
try {
|
|
9
|
+
await redisClient.connect();
|
|
10
|
+
console.log('✅ Redis connected successfully');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('❌ Redis connection error:', error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
redisClient.on('error', (error) => {
|
|
18
|
+
console.error('❌ Redis error:', error);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
redisClient.on('disconnect', () => {
|
|
22
|
+
console.log('⚠️ Redis disconnected');
|
|
23
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { cors } from '@elysiajs/cors';
|
|
3
|
+
import { swagger } from '@elysiajs/swagger';
|
|
4
|
+
import { connectDB } from './config/database';
|
|
5
|
+
import { connectRedis } from './config/redis';
|
|
6
|
+
import { userRoutes } from './routes/user.routes';
|
|
7
|
+
|
|
8
|
+
const app = new Elysia()
|
|
9
|
+
.use(cors())
|
|
10
|
+
.use(swagger({
|
|
11
|
+
documentation: {
|
|
12
|
+
info: {
|
|
13
|
+
title: 'Yiougo API',
|
|
14
|
+
version: '1.0.0'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}))
|
|
18
|
+
.get('/api/health', () => ({
|
|
19
|
+
status: 'ok',
|
|
20
|
+
timestamp: new Date().toISOString()
|
|
21
|
+
}))
|
|
22
|
+
.use(userRoutes)
|
|
23
|
+
.listen(process.env.PORT || 3000);
|
|
24
|
+
|
|
25
|
+
await connectDB();
|
|
26
|
+
await connectRedis();
|
|
27
|
+
|
|
28
|
+
console.log(`🚀 Server running at http://${app.server?.hostname}:${app.server?.port}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Elysia, t } from 'elysia';
|
|
2
|
+
import { UserService } from '../services/user.service';
|
|
3
|
+
|
|
4
|
+
export const userRoutes = new Elysia({ prefix: '/api/users' })
|
|
5
|
+
.get('/', async () => {
|
|
6
|
+
const users = await UserService.getAllUsers();
|
|
7
|
+
return { success: true, data: users };
|
|
8
|
+
})
|
|
9
|
+
.get('/:id', async ({ params: { id } }) => {
|
|
10
|
+
const user = await UserService.getUserById(id);
|
|
11
|
+
if (!user) {
|
|
12
|
+
return { success: false, error: 'User not found' };
|
|
13
|
+
}
|
|
14
|
+
return { success: true, data: user };
|
|
15
|
+
})
|
|
16
|
+
.post('/', async ({ body }) => {
|
|
17
|
+
const user = await UserService.createUser(body);
|
|
18
|
+
return { success: true, data: user };
|
|
19
|
+
}, {
|
|
20
|
+
body: t.Object({
|
|
21
|
+
username: t.String(),
|
|
22
|
+
email: t.String(),
|
|
23
|
+
password: t.String()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
.put('/:id', async ({ params: { id }, body }) => {
|
|
27
|
+
const user = await UserService.updateUser(id, body);
|
|
28
|
+
if (!user) {
|
|
29
|
+
return { success: false, error: 'User not found' };
|
|
30
|
+
}
|
|
31
|
+
return { success: true, data: user };
|
|
32
|
+
}, {
|
|
33
|
+
body: t.Object({
|
|
34
|
+
username: t.Optional(t.String()),
|
|
35
|
+
email: t.Optional(t.String()),
|
|
36
|
+
password: t.Optional(t.String())
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
.delete('/:id', async ({ params: { id } }) => {
|
|
40
|
+
const deleted = await UserService.deleteUser(id);
|
|
41
|
+
if (!deleted) {
|
|
42
|
+
return { success: false, error: 'User not found' };
|
|
43
|
+
}
|
|
44
|
+
return { success: true, message: 'User deleted successfully' };
|
|
45
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb';
|
|
2
|
+
import { IUser } from '../models/user.model';
|
|
3
|
+
import { getDB } from '../config/database';
|
|
4
|
+
import { redisClient } from '../config/redis';
|
|
5
|
+
|
|
6
|
+
const CACHE_TTL = 3600;
|
|
7
|
+
const COLLECTION_NAME = 'users';
|
|
8
|
+
|
|
9
|
+
export class UserService {
|
|
10
|
+
static async getAllUsers(): Promise<IUser[]> {
|
|
11
|
+
const cacheKey = 'users:all';
|
|
12
|
+
|
|
13
|
+
const cached = await redisClient.get(cacheKey);
|
|
14
|
+
if (cached) {
|
|
15
|
+
return JSON.parse(cached);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const db = getDB();
|
|
19
|
+
const users = await db.collection<IUser>(COLLECTION_NAME)
|
|
20
|
+
.find({}, { projection: { password: 0 } })
|
|
21
|
+
.toArray();
|
|
22
|
+
|
|
23
|
+
await redisClient.setEx(cacheKey, CACHE_TTL, JSON.stringify(users));
|
|
24
|
+
|
|
25
|
+
return users;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static async getUserById(id: string): Promise<IUser | null> {
|
|
29
|
+
const cacheKey = `user:${id}`;
|
|
30
|
+
|
|
31
|
+
const cached = await redisClient.get(cacheKey);
|
|
32
|
+
if (cached) {
|
|
33
|
+
return JSON.parse(cached);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const db = getDB();
|
|
37
|
+
const user = await db.collection<IUser>(COLLECTION_NAME)
|
|
38
|
+
.findOne({ _id: new ObjectId(id) }, { projection: { password: 0 } });
|
|
39
|
+
|
|
40
|
+
if (user) {
|
|
41
|
+
await redisClient.setEx(cacheKey, CACHE_TTL, JSON.stringify(user));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return user;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static async createUser(userData: Partial<IUser>): Promise<IUser> {
|
|
48
|
+
const db = getDB();
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const newUser: IUser = {
|
|
51
|
+
username: userData.username!,
|
|
52
|
+
email: userData.email!,
|
|
53
|
+
password: userData.password!,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = await db.collection<IUser>(COLLECTION_NAME).insertOne(newUser);
|
|
59
|
+
newUser._id = result.insertedId;
|
|
60
|
+
|
|
61
|
+
await redisClient.del('users:all');
|
|
62
|
+
|
|
63
|
+
return newUser;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static async updateUser(id: string, userData: Partial<IUser>): Promise<IUser | null> {
|
|
67
|
+
const db = getDB();
|
|
68
|
+
const updateData = {
|
|
69
|
+
...userData,
|
|
70
|
+
updatedAt: new Date()
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = await db.collection<IUser>(COLLECTION_NAME).findOneAndUpdate(
|
|
74
|
+
{ _id: new ObjectId(id) },
|
|
75
|
+
{ $set: updateData },
|
|
76
|
+
{ returnDocument: 'after', projection: { password: 0 } }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (result) {
|
|
80
|
+
await redisClient.del(`user:${id}`);
|
|
81
|
+
await redisClient.del('users:all');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static async deleteUser(id: string): Promise<boolean> {
|
|
88
|
+
const db = getDB();
|
|
89
|
+
const result = await db.collection<IUser>(COLLECTION_NAME)
|
|
90
|
+
.deleteOne({ _id: new ObjectId(id) });
|
|
91
|
+
|
|
92
|
+
if (result.deletedCount > 0) {
|
|
93
|
+
await redisClient.del(`user:${id}`);
|
|
94
|
+
await redisClient.del('users:all');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { User, ApiResponse } from '@shared/types'
|
|
2
|
+
|
|
3
|
+
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
|
|
4
|
+
|
|
5
|
+
async function request<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>> {
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(`${BASE_URL}${url}`, {
|
|
8
|
+
...options,
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
...options?.headers,
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const data = await response.json()
|
|
16
|
+
return data as ApiResponse<T>
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: error instanceof Error ? error.message : 'Network error'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const userApi = {
|
|
26
|
+
async getAll(): Promise<ApiResponse<User[]>> {
|
|
27
|
+
return request<User[]>('/api/users')
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async getById(id: string): Promise<ApiResponse<User>> {
|
|
31
|
+
return request<User>(`/api/users/${id}`)
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async create(userData: Partial<User>): Promise<ApiResponse<User>> {
|
|
35
|
+
return request<User>('/api/users', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
body: JSON.stringify(userData),
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async update(id: string, userData: Partial<User>): Promise<ApiResponse<User>> {
|
|
42
|
+
return request<User>(`/api/users/${id}`, {
|
|
43
|
+
method: 'PUT',
|
|
44
|
+
body: JSON.stringify(userData),
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async delete(id: string): Promise<ApiResponse<null>> {
|
|
49
|
+
return request<null>(`/api/users/${id}`, {
|
|
50
|
+
method: 'DELETE',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-btn
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
:loading="loading"
|
|
5
|
+
:disabled="disabled || loading"
|
|
6
|
+
class="base-button"
|
|
7
|
+
@click="handleClick"
|
|
8
|
+
>
|
|
9
|
+
<template v-if="loading">
|
|
10
|
+
<q-spinner-dots size="1em" />
|
|
11
|
+
<span class="q-ml-sm">{{ loadingText }}</span>
|
|
12
|
+
</template>
|
|
13
|
+
<template v-else>
|
|
14
|
+
<slot />
|
|
15
|
+
</template>
|
|
16
|
+
</q-btn>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
interface Props {
|
|
21
|
+
loading?: boolean
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
loadingText?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Emits {
|
|
27
|
+
(e: 'click', event: MouseEvent): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
31
|
+
loading: false,
|
|
32
|
+
disabled: false,
|
|
33
|
+
loadingText: 'Loading...'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits<Emits>()
|
|
37
|
+
|
|
38
|
+
function handleClick(event: MouseEvent) {
|
|
39
|
+
if (!props.disabled && !props.loading) {
|
|
40
|
+
emit('click', event)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style lang="scss" scoped>
|
|
46
|
+
.base-button {
|
|
47
|
+
min-width: 100px;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-card class="base-card" :class="{ 'base-card--flat': flat }">
|
|
3
|
+
<q-card-section v-if="$slots.header" class="base-card__header">
|
|
4
|
+
<slot name="header" />
|
|
5
|
+
</q-card-section>
|
|
6
|
+
|
|
7
|
+
<q-card-section v-if="$slots.default" class="base-card__content">
|
|
8
|
+
<slot />
|
|
9
|
+
</q-card-section>
|
|
10
|
+
|
|
11
|
+
<q-card-actions v-if="$slots.actions" class="base-card__actions">
|
|
12
|
+
<slot name="actions" />
|
|
13
|
+
</q-card-actions>
|
|
14
|
+
</q-card>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
interface Props {
|
|
19
|
+
flat?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
withDefaults(defineProps<Props>(), {
|
|
23
|
+
flat: false
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<style lang="scss" scoped>
|
|
28
|
+
.base-card {
|
|
29
|
+
&__header {
|
|
30
|
+
padding-bottom: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&__content {
|
|
34
|
+
padding-top: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&__actions {
|
|
38
|
+
padding-top: 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ref, computed, readonly } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface UseLoadingOptions {
|
|
4
|
+
initial?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function useLoading(options: UseLoadingOptions = {}) {
|
|
8
|
+
const { initial = false } = options
|
|
9
|
+
|
|
10
|
+
const loading = ref(initial)
|
|
11
|
+
|
|
12
|
+
const setLoading = (value: boolean) => {
|
|
13
|
+
loading.value = value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const startLoading = () => {
|
|
17
|
+
loading.value = true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const stopLoading = () => {
|
|
21
|
+
loading.value = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toggleLoading = () => {
|
|
25
|
+
loading.value = !loading.value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
loading: readonly(loading),
|
|
30
|
+
setLoading,
|
|
31
|
+
startLoading,
|
|
32
|
+
stopLoading,
|
|
33
|
+
toggleLoading,
|
|
34
|
+
isLoading: computed(() => loading.value)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useQuasar } from 'quasar'
|
|
2
|
+
|
|
3
|
+
export interface NotificationOptions {
|
|
4
|
+
type?: 'positive' | 'negative' | 'warning' | 'info'
|
|
5
|
+
message: string
|
|
6
|
+
timeout?: number
|
|
7
|
+
position?: 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useNotification() {
|
|
11
|
+
const $q = useQuasar()
|
|
12
|
+
|
|
13
|
+
const showNotification = (options: NotificationOptions) => {
|
|
14
|
+
const {
|
|
15
|
+
type = 'info',
|
|
16
|
+
message,
|
|
17
|
+
timeout = 3000,
|
|
18
|
+
position = 'top'
|
|
19
|
+
} = options
|
|
20
|
+
|
|
21
|
+
$q.notify({
|
|
22
|
+
type,
|
|
23
|
+
message,
|
|
24
|
+
timeout,
|
|
25
|
+
position
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const showSuccess = (message: string, options?: Omit<NotificationOptions, 'type' | 'message'>) => {
|
|
30
|
+
showNotification({ ...options, message, type: 'positive' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const showError = (message: string, options?: Omit<NotificationOptions, 'type' | 'message'>) => {
|
|
34
|
+
showNotification({ ...options, message, type: 'negative' })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const showWarning = (message: string, options?: Omit<NotificationOptions, 'type' | 'message'>) => {
|
|
38
|
+
showNotification({ ...options, message, type: 'warning' })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const showInfo = (message: string, options?: Omit<NotificationOptions, 'type' | 'message'>) => {
|
|
42
|
+
showNotification({ ...options, message, type: 'info' })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
showNotification,
|
|
47
|
+
showSuccess,
|
|
48
|
+
showError,
|
|
49
|
+
showWarning,
|
|
50
|
+
showInfo
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
/// <reference types="vite-plugin-pages/client" />
|
|
3
|
+
/// <reference types="vite-plugin-vue-layouts/client" />
|
|
4
|
+
|
|
5
|
+
interface ImportMetaEnv {
|
|
6
|
+
readonly VITE_API_URL: string
|
|
7
|
+
readonly BASE_URL: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ImportMeta {
|
|
11
|
+
readonly env: ImportMetaEnv
|
|
12
|
+
readonly url: string
|
|
13
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ref, readonly } from 'vue'
|
|
2
|
+
import type { ApiResponse } from '@shared/types'
|
|
3
|
+
|
|
4
|
+
export interface UseApiOptions<T> {
|
|
5
|
+
immediate?: boolean
|
|
6
|
+
onSuccess?: (data: T) => void
|
|
7
|
+
onError?: (error: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useApi<T = any>(
|
|
11
|
+
apiFunction: () => Promise<ApiResponse<T>>,
|
|
12
|
+
options: UseApiOptions<T> = {}
|
|
13
|
+
) {
|
|
14
|
+
const { immediate = false, onSuccess, onError } = options
|
|
15
|
+
|
|
16
|
+
const data = ref<T | null>(null)
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
const error = ref<string | null>(null)
|
|
19
|
+
|
|
20
|
+
const execute = async () => {
|
|
21
|
+
loading.value = true
|
|
22
|
+
error.value = null
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await apiFunction()
|
|
26
|
+
|
|
27
|
+
if (response.success && response.data) {
|
|
28
|
+
data.value = response.data
|
|
29
|
+
onSuccess?.(response.data)
|
|
30
|
+
} else {
|
|
31
|
+
error.value = response.error || 'Request failed'
|
|
32
|
+
onError?.(error.value)
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
|
36
|
+
error.value = errorMessage
|
|
37
|
+
onError?.(errorMessage)
|
|
38
|
+
} finally {
|
|
39
|
+
loading.value = false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const reset = () => {
|
|
44
|
+
data.value = null
|
|
45
|
+
error.value = null
|
|
46
|
+
loading.value = false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (immediate) {
|
|
50
|
+
execute()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
data: readonly(data),
|
|
55
|
+
loading: readonly(loading),
|
|
56
|
+
error: readonly(error),
|
|
57
|
+
execute,
|
|
58
|
+
reset
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-layout view="lHh Lpr lFf">
|
|
3
|
+
<q-header elevated class="bg-primary text-white">
|
|
4
|
+
<q-toolbar>
|
|
5
|
+
<q-toolbar-title>
|
|
6
|
+
<q-btn flat no-caps to="/" class="text-white">
|
|
7
|
+
Yiougo App
|
|
8
|
+
</q-btn>
|
|
9
|
+
</q-toolbar-title>
|
|
10
|
+
|
|
11
|
+
<q-space />
|
|
12
|
+
|
|
13
|
+
<q-tabs>
|
|
14
|
+
<q-route-tab to="/" label="首页" />
|
|
15
|
+
<q-route-tab to="/users" label="用户" />
|
|
16
|
+
<q-route-tab to="/about" label="关于" />
|
|
17
|
+
</q-tabs>
|
|
18
|
+
</q-toolbar>
|
|
19
|
+
</q-header>
|
|
20
|
+
|
|
21
|
+
<q-page-container>
|
|
22
|
+
<router-view />
|
|
23
|
+
</q-page-container>
|
|
24
|
+
</q-layout>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
// 默认布局组件
|
|
29
|
+
</script>
|