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.
Files changed (46) hide show
  1. package/README.md +120 -0
  2. package/index.js +76 -0
  3. package/package.json +45 -0
  4. package/template/.env.example +7 -0
  5. package/template/README.md +122 -0
  6. package/template/index.html +13 -0
  7. package/template/package-lock.json +3543 -0
  8. package/template/package.json +38 -0
  9. package/template/public/vite.svg +1 -0
  10. package/template/server/config/database.ts +31 -0
  11. package/template/server/config/redis.ts +23 -0
  12. package/template/server/index.ts +28 -0
  13. package/template/server/models/user.model.ts +10 -0
  14. package/template/server/routes/user.routes.ts +45 -0
  15. package/template/server/services/user.service.ts +100 -0
  16. package/template/shared/types/index.ts +14 -0
  17. package/template/src/App.vue +16 -0
  18. package/template/src/api/user.api.ts +53 -0
  19. package/template/src/components/BaseButton.vue +49 -0
  20. package/template/src/components/BaseCard.vue +41 -0
  21. package/template/src/components/index.ts +3 -0
  22. package/template/src/composables/index.ts +3 -0
  23. package/template/src/composables/useLoading.ts +36 -0
  24. package/template/src/composables/useNotification.ts +52 -0
  25. package/template/src/env.d.ts +13 -0
  26. package/template/src/hooks/index.ts +2 -0
  27. package/template/src/hooks/useApi.ts +60 -0
  28. package/template/src/layouts/default.vue +29 -0
  29. package/template/src/main.ts +20 -0
  30. package/template/src/pages/about.vue +111 -0
  31. package/template/src/pages/index.vue +38 -0
  32. package/template/src/pages/users.vue +126 -0
  33. package/template/src/router/index.ts +12 -0
  34. package/template/src/stores/user.store.ts +64 -0
  35. package/template/src/styles/main.scss +15 -0
  36. package/template/src/styles/quasar-variables.sass +8 -0
  37. package/template/src/types/global.d.ts +28 -0
  38. package/template/src/utils/date.ts +49 -0
  39. package/template/src/utils/index.ts +3 -0
  40. package/template/src/utils/storage.ts +56 -0
  41. package/template/src/vite-env.d.ts +7 -0
  42. package/template/tsconfig.app.json +32 -0
  43. package/template/tsconfig.json +7 -0
  44. package/template/tsconfig.node.json +23 -0
  45. package/template/tsconfig.server.json +29 -0
  46. 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,10 @@
1
+ import { ObjectId } from 'mongodb';
2
+
3
+ export interface IUser {
4
+ _id?: ObjectId;
5
+ username: string;
6
+ email: string;
7
+ password: string;
8
+ createdAt: Date;
9
+ updatedAt: Date;
10
+ }
@@ -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,14 @@
1
+ export interface User {
2
+ _id: string;
3
+ username: string;
4
+ email: string;
5
+ createdAt: string;
6
+ updatedAt: string;
7
+ }
8
+
9
+ export interface ApiResponse<T> {
10
+ success: boolean;
11
+ data?: T;
12
+ error?: string;
13
+ message?: string;
14
+ }
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <router-view />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ </script>
7
+
8
+ <style lang="scss">
9
+ html,
10
+ body,
11
+ #app {
12
+ height: 100%;
13
+ width: 100%;
14
+ overflow: hidden;
15
+ }
16
+ </style>
@@ -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,3 @@
1
+ // 基础组件导出
2
+ export { default as BaseCard } from './BaseCard.vue'
3
+ export { default as BaseButton } from './BaseButton.vue'
@@ -0,0 +1,3 @@
1
+ // 组合式 API 导出
2
+ export { useLoading } from './useLoading'
3
+ export { useNotification } from './useNotification'
@@ -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,2 @@
1
+ // Hooks 导出
2
+ export { useApi } from './useApi'
@@ -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>