first commit
This commit is contained in:
98
resources/js/composables/useAppDialog.ts
Normal file
98
resources/js/composables/useAppDialog.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Очередь подтверждающих диалогов с поддержкой async-onConfirm и анимаций.
|
||||
* Перенесено из проекта onboard (Composables/useAppDialog.js).
|
||||
*
|
||||
* Использование:
|
||||
* const ok = await useAppDialog({ title: 'Удалить?', content: '...' })
|
||||
*/
|
||||
export interface AppDialogButtonProps {
|
||||
type?: 'default' | 'primary' | 'info' | 'success' | 'warning' | 'error';
|
||||
secondary?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AppDialogItem {
|
||||
id: number;
|
||||
show: boolean;
|
||||
title?: string;
|
||||
content?: string;
|
||||
loading: boolean;
|
||||
onConfirm?: () => unknown | Promise<unknown>;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positiveProps?: AppDialogButtonProps;
|
||||
negativeProps?: AppDialogButtonProps;
|
||||
maskClosable?: boolean;
|
||||
resolve: (confirmed: boolean) => void;
|
||||
}
|
||||
|
||||
export interface AppDialogOptions {
|
||||
title?: string;
|
||||
content?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positiveProps?: AppDialogButtonProps;
|
||||
negativeProps?: AppDialogButtonProps;
|
||||
maskClosable?: boolean;
|
||||
onConfirm?: () => unknown | Promise<unknown>;
|
||||
}
|
||||
|
||||
export const dialogQueue = ref<AppDialogItem[]>([]);
|
||||
let idCounter = 0;
|
||||
|
||||
/** Закрытие диалога (кнопка / Esc / клик по маске). */
|
||||
export function closeDialog(id: number, confirmed = false): void {
|
||||
const dialog = dialogQueue.value.find((d) => d.id === id);
|
||||
|
||||
if (dialog && dialog.show) {
|
||||
dialog.show = false;
|
||||
dialog.resolve(confirmed);
|
||||
}
|
||||
}
|
||||
|
||||
/** Удаление из очереди после завершения leave-анимации. */
|
||||
export function cleanupDialog(id: number): void {
|
||||
dialogQueue.value = dialogQueue.value.filter((d) => d.id !== id);
|
||||
}
|
||||
|
||||
export function useAppDialog(options: AppDialogOptions = {}): Promise<boolean> {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
positiveProps,
|
||||
negativeProps,
|
||||
positiveText = 'Подтвердить',
|
||||
negativeText = 'Отмена',
|
||||
maskClosable = false,
|
||||
onConfirm,
|
||||
} = options;
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const id = idCounter++;
|
||||
|
||||
dialogQueue.value.push({
|
||||
id,
|
||||
show: false,
|
||||
title,
|
||||
content,
|
||||
loading: false,
|
||||
onConfirm,
|
||||
positiveText,
|
||||
negativeText,
|
||||
positiveProps,
|
||||
negativeProps,
|
||||
maskClosable,
|
||||
resolve,
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
const dialog = dialogQueue.value.find((d) => d.id === id);
|
||||
|
||||
if (dialog) {
|
||||
dialog.show = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
57
resources/js/composables/useGlobalLoading.ts
Normal file
57
resources/js/composables/useGlobalLoading.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Глобальный индикатор загрузки. Баннер показывается только если операция длится
|
||||
* дольше SHOW_DELAY_MS — чтобы не мигать на быстрых переходах.
|
||||
*
|
||||
* Перенесено из проекта onboard (Composables/useGlobalLoading.js).
|
||||
*/
|
||||
const pending = ref(0);
|
||||
const visible = ref(false);
|
||||
let showTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const SHOW_DELAY_MS = 3000;
|
||||
|
||||
const scheduleShow = (): void => {
|
||||
if (visible.value || showTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
showTimer = setTimeout(() => {
|
||||
showTimer = null;
|
||||
|
||||
if (pending.value > 0) {
|
||||
visible.value = true;
|
||||
}
|
||||
}, SHOW_DELAY_MS);
|
||||
};
|
||||
|
||||
const cancelScheduledShow = (): void => {
|
||||
if (!showTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(showTimer);
|
||||
showTimer = null;
|
||||
};
|
||||
|
||||
export const startGlobalLoading = (): void => {
|
||||
pending.value += 1;
|
||||
scheduleShow();
|
||||
};
|
||||
|
||||
export const stopGlobalLoading = (): void => {
|
||||
pending.value = Math.max(0, pending.value - 1);
|
||||
|
||||
if (pending.value === 0) {
|
||||
cancelScheduledShow();
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGlobalLoading = () => {
|
||||
return {
|
||||
isGlobalLoading: computed(() => visible.value),
|
||||
pendingCount: computed(() => pending.value),
|
||||
};
|
||||
};
|
||||
41
resources/js/composables/useNotification.ts
Normal file
41
resources/js/composables/useNotification.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { NotificationOptions } from 'naive-ui';
|
||||
|
||||
/**
|
||||
* Обёртка над discrete notification API NaiveUI с удобными хелперами.
|
||||
* Перенесено и упрощено из проекта onboard (Composables/useNotification.js).
|
||||
*/
|
||||
export function useNotification() {
|
||||
const showNotification = (options: NotificationOptions) => {
|
||||
return window.$notification?.create(options) ?? null;
|
||||
};
|
||||
|
||||
const success = (
|
||||
title: string,
|
||||
description = '',
|
||||
options: Partial<NotificationOptions> = {},
|
||||
) => {
|
||||
return showNotification({
|
||||
title,
|
||||
description,
|
||||
meta: options.meta ?? new Date().toLocaleDateString(),
|
||||
...options,
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const error = (
|
||||
title = 'Произошла ошибка',
|
||||
description = '',
|
||||
options: Partial<NotificationOptions> = {},
|
||||
) => {
|
||||
return showNotification({
|
||||
title,
|
||||
description,
|
||||
meta: options.meta ?? new Date().toLocaleDateString(),
|
||||
...options,
|
||||
type: 'error',
|
||||
});
|
||||
};
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
Reference in New Issue
Block a user