first init
This commit is contained in:
9
src/App.vue
Normal file
9
src/App.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
19
src/apis/user.ts
Normal file
19
src/apis/user.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import http from "../utils/http";
|
||||
|
||||
/**
|
||||
* This is an example, please remove if not needed
|
||||
*/
|
||||
export const getUserList = () => {
|
||||
return http({
|
||||
url: "/user/list",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const insertUser = (data: { account: string; passowrd: string }) => {
|
||||
return http({
|
||||
url: "/user/insert",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
BIN
src/assets/hucky.png
Normal file
BIN
src/assets/hucky.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 888 KiB |
16
src/hooks/bus/changeUserBus.ts
Normal file
16
src/hooks/bus/changeUserBus.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createEventHook } from "@vueuse/core";
|
||||
|
||||
export interface ChangeUserBus {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const changeUserEvent = createEventHook<ChangeUserBus>();
|
||||
|
||||
/**
|
||||
* This is an example, please remove if not needed
|
||||
*/
|
||||
export const changeUserBus = {
|
||||
emit: changeUserEvent.emit,
|
||||
on: changeUserEvent.on,
|
||||
off: changeUserEvent.off,
|
||||
};
|
||||
11
src/main.ts
Normal file
11
src/main.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createApp(App).use(router).use(pinia).mount("#app");
|
||||
19
src/pages/AboutPage.vue
Normal file
19
src/pages/AboutPage.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
// About page component
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">关于我们</h1>
|
||||
<p class="py-6">这是一个自动路由注册的示例页面</p>
|
||||
<p>路径将自动转换为 /about</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component styles */
|
||||
</style>
|
||||
77
src/pages/HelpPage.vue
Normal file
77
src/pages/HelpPage.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts" setup>
|
||||
import { range } from "radash";
|
||||
import { usePagination } from "@/utils/pagination";
|
||||
|
||||
const merchandise = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: "商品1",
|
||||
},
|
||||
]);
|
||||
|
||||
const { currentPage, changePage, pageNumbers, totalPages, getPaginatedData } =
|
||||
usePagination(() => merchandise.value);
|
||||
const handlePageChange = (page: number) => {
|
||||
changePage(page);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await new Promise(() => {
|
||||
setTimeout(() => {
|
||||
merchandise.value = Array.from(range(1, 50)).map((item) => ({
|
||||
id: item,
|
||||
name: `商品${item}`,
|
||||
}));
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">帮助</h1>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li v-for="item in getPaginatedData" :key="item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:disabled="currentPage === 1"
|
||||
@click="handlePageChange(currentPage - 1)"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-for="page in pageNumbers"
|
||||
:key="page"
|
||||
class="join-item btn btn-sm"
|
||||
:class="{
|
||||
'btn-active': page === currentPage,
|
||||
'btn-disabled': page === '...',
|
||||
}"
|
||||
@click="page !== '...' && handlePageChange(Number(page))"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="handlePageChange(currentPage + 1)"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component styles */
|
||||
</style>
|
||||
18
src/pages/HomePage.vue
Normal file
18
src/pages/HomePage.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
// Home page component
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">首页</h1>
|
||||
<p class="py-6">欢迎使用自动路由系统</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component styles */
|
||||
</style>
|
||||
15
src/pages/panel/MePage.vue
Normal file
15
src/pages/panel/MePage.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">我的</h1>
|
||||
<p class="py-6">这是一个自动路由注册的示例页面</p>
|
||||
<p>路径将自动转换为 /panel/me</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
98
src/router/index.ts
Normal file
98
src/router/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
|
||||
/**
|
||||
* Convert PascalCase to kebab-case and remove 'Page' suffix
|
||||
* @param name PascalCase name (e.g., AboutMePage)
|
||||
* @returns kebab-case path (e.g., about-me)
|
||||
*/
|
||||
function formatPathFromComponentName(name: string): string {
|
||||
// Remove .vue extension if present
|
||||
const baseName = name.endsWith(".vue") ? name.slice(0, -4) : name;
|
||||
|
||||
// Remove Page suffix if present
|
||||
const withoutPageSuffix = baseName.endsWith("Page")
|
||||
? baseName.slice(0, -4)
|
||||
: baseName;
|
||||
|
||||
// Convert PascalCase to kebab-case
|
||||
return withoutPageSuffix
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract component name from file path
|
||||
* @param filePath Path to the component file
|
||||
* @returns Component name without extension
|
||||
*/
|
||||
function getComponentNameFromPath(filePath: string): string {
|
||||
// Get the file name from the path
|
||||
return filePath.split("/").pop() || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate route path from full file path, preserving directory structure
|
||||
* @param filePath Full path to the component file (e.g., "@/pages/test/AbcPage.vue")
|
||||
* @returns Route path (e.g., "/test/abc")
|
||||
*/
|
||||
function generateRoutePathFromFilePath(filePath: string): string {
|
||||
// Remove the "@/pages" prefix and get the relative path
|
||||
const relativePath = filePath.replace(/^@\/pages\//, "");
|
||||
|
||||
// Split into directory parts and filename
|
||||
const pathParts = relativePath.split("/");
|
||||
const fileName = pathParts.pop() || "";
|
||||
const directories = pathParts.splice(3);
|
||||
|
||||
// Transform the filename using existing logic
|
||||
const transformedFileName = formatPathFromComponentName(fileName);
|
||||
|
||||
// Combine directory path with transformed filename
|
||||
const fullPath =
|
||||
directories.length > 0
|
||||
? `/${directories.join("/")}/${transformedFileName}`
|
||||
: `/${transformedFileName}`;
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate routes from page components using Vite's import.meta.glob
|
||||
* @returns Array of route configurations
|
||||
*/
|
||||
function generateRoutesFromPages(): RouteRecordRaw[] {
|
||||
const routes: RouteRecordRaw[] = [];
|
||||
|
||||
const pages = import.meta.glob("@/pages/**/*.vue");
|
||||
|
||||
for (const path in pages) {
|
||||
const componentName = getComponentNameFromPath(path);
|
||||
const routePath = generateRoutePathFromFilePath(path);
|
||||
|
||||
// Special case for home page
|
||||
const finalPath = routePath.toLowerCase() === "/home" ? "/" : routePath;
|
||||
|
||||
routes.push({
|
||||
path: finalPath,
|
||||
name: componentName,
|
||||
component: pages[path],
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
// Generate routes from pages directory
|
||||
const routes: RouteRecordRaw[] = generateRoutesFromPages();
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
21
src/stores/UserStore.ts
Normal file
21
src/stores/UserStore.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
"user",
|
||||
() => {
|
||||
const token = ref("");
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken;
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
setToken,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
);
|
||||
29
src/style.css
Normal file
29
src/style.css
Normal file
@@ -0,0 +1,29 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
||||
/* Custom Scrollbar Styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar styles */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
5
src/types/http.d.ts
vendored
Normal file
5
src/types/http.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare interface Result<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
92
src/utils/confirm.tsx
Normal file
92
src/utils/confirm.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createApp, ref } from "vue";
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
onConfirm?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export function confirm(options: ConfirmOptions): void {
|
||||
const confirmHideFlag = ref(false);
|
||||
const defaultOptions = {
|
||||
title: "确认",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {},
|
||||
...options,
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
confirmHideFlag.value = true;
|
||||
defaultOptions.onConfirm?.();
|
||||
setTimeout(() => {
|
||||
confirmApp.unmount();
|
||||
document.body.removeChild(confirmContainer);
|
||||
}, 300); // 动画结束后移除
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
confirmHideFlag.value = true;
|
||||
defaultOptions.onCancel?.();
|
||||
setTimeout(() => {
|
||||
confirmApp.unmount();
|
||||
document.body.removeChild(confirmContainer);
|
||||
}, 300); // 动画结束后移除
|
||||
};
|
||||
|
||||
const confirmInstance = () => (
|
||||
<AnimatePresence>
|
||||
{confirmHideFlag.value ? null : (
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 bg-[#00000050]">
|
||||
<motion.div
|
||||
class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 px-2"
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
>
|
||||
{defaultOptions.title && (
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{defaultOptions.title}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-gray-700">{defaultOptions.message}</p>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex justify-end space-x-3">
|
||||
{defaultOptions.cancelText && (
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{defaultOptions.cancelText}
|
||||
</button>
|
||||
)}
|
||||
{defaultOptions.confirmText && (
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{defaultOptions.confirmText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
const confirmContainer = document.createElement("div");
|
||||
document.body.appendChild(confirmContainer);
|
||||
|
||||
const confirmApp = createApp(confirmInstance);
|
||||
confirmApp.mount(confirmContainer);
|
||||
}
|
||||
26
src/utils/http.ts
Normal file
26
src/utils/http.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import { useUserStore } from "@/stores/UserStore.ts";
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_SERVER,
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
const store = useUserStore();
|
||||
if (!store.token) {
|
||||
return config;
|
||||
}
|
||||
const token = store.token;
|
||||
if (!token) {
|
||||
return config;
|
||||
}
|
||||
config.headers["token"] = token;
|
||||
return config;
|
||||
});
|
||||
|
||||
const http = async <T>(config: AxiosRequestConfig): Promise<Result<T>> => {
|
||||
const { data } = await instance.request<Result<T>>(config);
|
||||
return data;
|
||||
};
|
||||
|
||||
export default http;
|
||||
158
src/utils/pagination.ts
Normal file
158
src/utils/pagination.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ref, computed, ComputedRef, Ref } from "vue";
|
||||
|
||||
export interface PaginationOptions {
|
||||
pageSize?: number;
|
||||
initialPage?: number;
|
||||
maxVisiblePages?: number;
|
||||
}
|
||||
|
||||
export interface PaginationResult<T> {
|
||||
// Pagination state
|
||||
currentPage: Ref<number>;
|
||||
pageSize: Ref<number>;
|
||||
totalPages: ComputedRef<number>;
|
||||
pageNumbers: ComputedRef<(number | string)[]>;
|
||||
|
||||
// Pagination methods
|
||||
changePage: (page: number) => void;
|
||||
nextPage: () => void;
|
||||
prevPage: () => void;
|
||||
|
||||
// Data methods
|
||||
getPaginatedData: ComputedRef<T[]>;
|
||||
setTotalItems: (count: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pagination functionality for a list of items
|
||||
*
|
||||
* @param data Ref to the full data array or a function that returns the full data
|
||||
* @param options Pagination options
|
||||
* @returns Pagination state and methods
|
||||
*/
|
||||
export function usePagination<T>(
|
||||
data: Ref<T[]> | (() => T[]),
|
||||
options: PaginationOptions = {},
|
||||
): PaginationResult<T> {
|
||||
// Initialize pagination state
|
||||
const currentPage = ref(options.initialPage || 1);
|
||||
const pageSize = ref(options.pageSize || 10);
|
||||
const maxVisiblePages = options.maxVisiblePages || 5;
|
||||
|
||||
// Calculate if data is a ref or a function
|
||||
const isDataRef = "value" in data;
|
||||
|
||||
// Calculate total items
|
||||
const totalItems = computed(() => {
|
||||
if (isDataRef) {
|
||||
return (data as Ref<T[]>).value.length;
|
||||
} else {
|
||||
return (data as () => T[])().length;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate total pages
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalItems.value / pageSize.value);
|
||||
});
|
||||
|
||||
// Get paginated data
|
||||
const getPaginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value;
|
||||
const end = start + pageSize.value;
|
||||
|
||||
if (isDataRef) {
|
||||
return (data as Ref<T[]>).value.slice(start, end);
|
||||
} else {
|
||||
const fullData = (data as () => T[])();
|
||||
return fullData.slice(start, end);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate array of page numbers for pagination
|
||||
const pageNumbers = computed(() => {
|
||||
const pages: (number | string)[] = [];
|
||||
|
||||
if (totalPages.value <= maxVisiblePages) {
|
||||
// Show all pages if total pages are less than max visible
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
pages.push(1);
|
||||
|
||||
// Calculate start and end of visible page range
|
||||
let startPage = Math.max(2, currentPage.value - 1);
|
||||
let endPage = Math.min(totalPages.value - 1, currentPage.value + 1);
|
||||
|
||||
// Adjust if we're near the beginning
|
||||
if (currentPage.value <= 3) {
|
||||
endPage = Math.min(totalPages.value - 1, 4);
|
||||
}
|
||||
|
||||
// Adjust if we're near the end
|
||||
if (currentPage.value >= totalPages.value - 2) {
|
||||
startPage = Math.max(2, totalPages.value - 3);
|
||||
}
|
||||
|
||||
// Add ellipsis if needed before visible pages
|
||||
if (startPage > 2) {
|
||||
pages.push("...");
|
||||
}
|
||||
|
||||
// Add visible page numbers
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
// Add ellipsis if needed after visible pages
|
||||
if (endPage < totalPages.value - 1) {
|
||||
pages.push("...");
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
pages.push(totalPages.value);
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
// Change page
|
||||
const changePage = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
};
|
||||
|
||||
// Next page
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < totalPages.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
// Previous page
|
||||
const prevPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// Set total items (useful for API pagination)
|
||||
const setTotalItems = (count: number) => {
|
||||
totalItems.value = count;
|
||||
};
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
pageNumbers,
|
||||
changePage,
|
||||
nextPage,
|
||||
prevPage,
|
||||
getPaginatedData,
|
||||
setTotalItems,
|
||||
};
|
||||
}
|
||||
50
src/utils/toast.tsx
Normal file
50
src/utils/toast.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createApp, ref } from "vue";
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
|
||||
export interface ToastOptions {
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export function toast(options: ToastOptions): void {
|
||||
const toastHideFlag = ref(false);
|
||||
|
||||
const toastInstance = () => (
|
||||
<AnimatePresence>
|
||||
{toastHideFlag.value ? null : (
|
||||
<motion.div
|
||||
class="toast toast-top toast-end bg-transparent"
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
>
|
||||
<div class="alert">
|
||||
<span>{options.message}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
const toastContainer = document.createElement("div");
|
||||
document.body.appendChild(toastContainer);
|
||||
|
||||
const toastApp = createApp(toastInstance);
|
||||
toastApp.mount(toastContainer);
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
toastHideFlag.value = true;
|
||||
},
|
||||
(options.duration || 3000) + 250, // 250ms for start animation
|
||||
);
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
toastApp.unmount();
|
||||
document.body.removeChild(toastContainer);
|
||||
},
|
||||
(options.duration || 3000) + 500, // 500ms for end animation
|
||||
);
|
||||
}
|
||||
9
src/vite-env.d.ts
vendored
Normal file
9
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user