feat: 完善前后端核心功能模块
后端: - 新增认证(auth)、任务(tasks)、映射(mappings)、对账(reconciliation)、异常(exceptions)、导出(exports) API - 新增核心模块: database, security, permissions, tenant, exceptions, error_handlers - 新增数据模型: user, company, reconciliation_task, field_mapping, uploaded_file 等 - 新增服务层: ai_recognizer, file_parser, file_storage, mapping, reconciliation 等 - 添加数据库迁移脚本 前端: - 新增登录页面和仪表盘页面 - 新增任务列表、任务详情、字段映射页面 - 新增异常处理页面和规则设置页面 - 新增 API 代理路由 /api/[...path] - 新增 UI 组件库 (button, card, dialog, input, table 等) - 新增 auth 组件 (ProtectedRoute, PermissionGate) - 新增 layout 组件 (Header, Sidebar) - 新增 mapping 组件 (FieldMappingTable, AISuggestionPanel) - 新增 API 客户端和 hooks (useAsync, useToast, usePermission 等) - 新增状态管理 (auth-store, company-store, ui-store) - 集成 Tailwind CSS 和 shadcn/ui 组件库 其他: - 添加 Alembic 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
/**
|
||||
* 异步请求状态
|
||||
*/
|
||||
export interface AsyncState<T> {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步请求 Hook
|
||||
* 用于管理异步请求的加载状态、数据和错误
|
||||
*/
|
||||
export function useAsync<T = unknown>() {
|
||||
const [state, setState] = useState<AsyncState<T>>({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const execute = useCallback(async (asyncFunction: () => Promise<T>) => {
|
||||
setState({ data: null, loading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await asyncFunction();
|
||||
setState({ data: result, loading: false, error: null });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
setState({ data: null, loading: false, error: err });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState({ data: null, loading: false, error: null });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
execute,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export default useAsync;
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
/**
|
||||
* 确认对话框配置
|
||||
*/
|
||||
export interface ConfirmOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
type?: 'info' | 'warning' | 'danger';
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框状态
|
||||
*/
|
||||
interface ConfirmState {
|
||||
isOpen: boolean;
|
||||
options: ConfirmOptions | null;
|
||||
resolve: ((value: boolean) => void) | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框 Hook
|
||||
* 用于显示确认对话框并等待用户响应
|
||||
*/
|
||||
export function useConfirm() {
|
||||
const [state, setState] = useState<ConfirmState>({
|
||||
isOpen: false,
|
||||
options: null,
|
||||
resolve: null,
|
||||
});
|
||||
|
||||
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
setState({
|
||||
isOpen: true,
|
||||
options: {
|
||||
title: '确认',
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
type: 'info',
|
||||
...options,
|
||||
},
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (state.resolve) {
|
||||
state.resolve(true);
|
||||
}
|
||||
setState({ isOpen: false, options: null, resolve: null });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state.resolve]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (state.resolve) {
|
||||
state.resolve(false);
|
||||
}
|
||||
setState({ isOpen: false, options: null, resolve: null });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state.resolve]);
|
||||
|
||||
return {
|
||||
confirm,
|
||||
isOpen: state.isOpen,
|
||||
options: state.options,
|
||||
handleConfirm,
|
||||
handleCancel,
|
||||
};
|
||||
}
|
||||
|
||||
export default useConfirm;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* 防抖 Hook
|
||||
* 延迟执行函数,直到停止调用一段时间后才执行
|
||||
*
|
||||
* @param value - 要防抖的值
|
||||
* @param delay - 延迟时间(毫秒)
|
||||
* @returns 防抖后的值
|
||||
*/
|
||||
export function useDebounce<T>(value: T, delay: number = 500): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖回调 Hook
|
||||
* 延迟执行回调函数
|
||||
*
|
||||
* @param callback - 要防抖的回调函数
|
||||
* @param delay - 延迟时间(毫秒)
|
||||
* @returns 防抖后的回调函数
|
||||
*/
|
||||
export function useDebouncedCallback<T extends (...args: unknown[]) => unknown>(
|
||||
callback: T,
|
||||
delay: number = 500
|
||||
): (...args: Parameters<T>) => void {
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const callbackRef = useRef<T>(callback);
|
||||
|
||||
// 保持最新的 callback 引用
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useCallback(
|
||||
(...args: Parameters<T>) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
callbackRef.current(...args);
|
||||
}, delay);
|
||||
},
|
||||
[delay]
|
||||
);
|
||||
}
|
||||
|
||||
export default useDebounce;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
|
||||
/**
|
||||
* 权限检查 Hook
|
||||
*/
|
||||
export function usePermission() {
|
||||
const { user } = useAuthStore();
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定权限
|
||||
*/
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
if (!user) return false;
|
||||
return user.permissions?.includes(permission) ?? false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定角色
|
||||
*/
|
||||
const hasRole = (role: string): boolean => {
|
||||
if (!user) return false;
|
||||
return user.role === role;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否有任一权限
|
||||
*/
|
||||
const hasAnyPermission = (permissions: string[]): boolean => {
|
||||
if (!user) return false;
|
||||
return permissions.some((permission) => hasPermission(permission));
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有所有权限
|
||||
*/
|
||||
const hasAllPermissions = (permissions: string[]): boolean => {
|
||||
if (!user) return false;
|
||||
return permissions.every((permission) => hasPermission(permission));
|
||||
};
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePermission;
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Toast 消息类型
|
||||
*/
|
||||
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
||||
|
||||
/**
|
||||
* Toast 消息接口
|
||||
*/
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast Hook
|
||||
* 用于显示全局通知消息
|
||||
*/
|
||||
export function useToast() {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||
}, []);
|
||||
|
||||
const addToast = useCallback((type: ToastType, message: string, duration: number = 3000) => {
|
||||
const id = `toast-${Date.now()}-${Math.random()}`;
|
||||
const toast: Toast = { id, type, message, duration };
|
||||
|
||||
setToasts((prev) => [...prev, toast]);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}, [removeToast]);
|
||||
|
||||
const success = useCallback(
|
||||
(message: string, duration?: number) => {
|
||||
return addToast('success', message, duration);
|
||||
},
|
||||
[addToast]
|
||||
);
|
||||
|
||||
const error = useCallback(
|
||||
(message: string, duration?: number) => {
|
||||
return addToast('error', message, duration);
|
||||
},
|
||||
[addToast]
|
||||
);
|
||||
|
||||
const info = useCallback(
|
||||
(message: string, duration?: number) => {
|
||||
return addToast('info', message, duration);
|
||||
},
|
||||
[addToast]
|
||||
);
|
||||
|
||||
const warning = useCallback(
|
||||
(message: string, duration?: number) => {
|
||||
return addToast('warning', message, duration);
|
||||
},
|
||||
[addToast]
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setToasts([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
toasts,
|
||||
addToast,
|
||||
removeToast,
|
||||
success,
|
||||
error,
|
||||
info,
|
||||
warning,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
|
||||
export default useToast;
|
||||
Reference in New Issue
Block a user