first init

This commit is contained in:
2025-11-09 20:01:55 +08:00
commit 4c131cbc38
35 changed files with 2298 additions and 0 deletions

92
src/utils/confirm.tsx Normal file
View 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
View 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
View 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
View 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
);
}