first init
This commit is contained in:
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user