Merge branch 'fork' of 124-template-94/d8d-admin-mobile-starter-public into main
This commit is contained in:
@@ -3,5 +3,6 @@
|
||||
迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开
|
||||
|
||||
2025.05.13 0.1.0
|
||||
首页添加了迁移管理入口按钮, 无需登录即可访问
|
||||
打开迁移管理页面时,将迁移历史读取出来
|
||||
将admin api.ts 拆开
|
||||
打开迁移管理页面时,将迁移历史读取出来
|
||||
首页添加了迁移管理入口按钮, 无需登录即可访问
|
||||
@@ -1,782 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { getGlobalConfig } from './utils.ts';
|
||||
import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import type {
|
||||
User, FileLibrary, FileCategory, ThemeSettings,
|
||||
SystemSetting, SystemSettingGroupData,
|
||||
LoginLocation, LoginLocationDetail,
|
||||
Message, UserMessage, KnowInfo
|
||||
} from '../share/types.ts';
|
||||
|
||||
|
||||
|
||||
// 定义API基础URL
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
// 获取OSS完整URL
|
||||
export const getOssUrl = (path: string): string => {
|
||||
// 获取全局配置中的OSS_HOST,如果不存在使用默认值
|
||||
const ossHost = getGlobalConfig('OSS_BASE_URL') || '';
|
||||
// 确保path不以/开头
|
||||
const ossPath = path.startsWith('/') ? path.substring(1) : path;
|
||||
return `${ossHost}/${ossPath}`;
|
||||
};
|
||||
|
||||
// ===================
|
||||
// Auth API 定义部分
|
||||
// ===================
|
||||
|
||||
// 定义API返回数据类型
|
||||
interface AuthLoginResponse {
|
||||
message: string;
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
message: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 定义Auth API接口类型
|
||||
interface AuthAPIType {
|
||||
login: (username: string, password: string, latitude?: number, longitude?: number) => Promise<AuthLoginResponse>;
|
||||
register: (username: string, email: string, password: string) => Promise<AuthResponse>;
|
||||
logout: () => Promise<AuthResponse>;
|
||||
getCurrentUser: () => Promise<User>;
|
||||
updateUser: (userId: number, userData: Partial<User>) => Promise<User>;
|
||||
changePassword: (oldPassword: string, newPassword: string) => Promise<AuthResponse>;
|
||||
requestPasswordReset: (email: string) => Promise<AuthResponse>;
|
||||
resetPassword: (token: string, newPassword: string) => Promise<AuthResponse>;
|
||||
}
|
||||
|
||||
|
||||
// Auth相关API
|
||||
export const AuthAPI: AuthAPIType = {
|
||||
// 登录API
|
||||
login: async (username: string, password: string, latitude?: number, longitude?: number) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/login`, {
|
||||
username,
|
||||
password,
|
||||
latitude,
|
||||
longitude
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 注册API
|
||||
register: async (username: string, email: string, password: string) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/register`, { username, email, password });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 登出API
|
||||
logout: async () => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/logout`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
getCurrentUser: async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/auth/me`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUser: async (userId: number, userData: Partial<User>) => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/auth/users/${userId}`, userData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
changePassword: async (oldPassword: string, newPassword: string) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/change-password`, { oldPassword, newPassword });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 请求重置密码
|
||||
requestPasswordReset: async (email: string) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/request-password-reset`, { email });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 重置密码
|
||||
resetPassword: async (token: string, newPassword: string) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/reset-password`, { token, newPassword });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 为UserAPI添加的接口响应类型
|
||||
interface UsersResponse {
|
||||
data: User[];
|
||||
pagination: {
|
||||
total: number;
|
||||
current: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
data: User;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface UserCreateResponse {
|
||||
message: string;
|
||||
data: User;
|
||||
}
|
||||
|
||||
interface UserUpdateResponse {
|
||||
message: string;
|
||||
data: User;
|
||||
}
|
||||
|
||||
interface UserDeleteResponse {
|
||||
message: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
// 用户管理API
|
||||
export const UserAPI = {
|
||||
// 获取用户列表
|
||||
getUsers: async (params?: { page?: number, limit?: number, search?: string }): Promise<UsersResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/users`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个用户详情
|
||||
getUser: async (userId: number): Promise<UserResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 创建用户
|
||||
createUser: async (userData: Partial<User>): Promise<UserCreateResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/users`, userData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUser: async (userId: number, userData: Partial<User>): Promise<UserUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/users/${userId}`, userData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
deleteUser: async (userId: number): Promise<UserDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 定义文件相关接口类型
|
||||
interface FileUploadPolicyResponse {
|
||||
message: string;
|
||||
data: MinioUploadPolicy | OSSUploadPolicy;
|
||||
}
|
||||
|
||||
interface FileListResponse {
|
||||
message: string;
|
||||
data: {
|
||||
list: FileLibrary[];
|
||||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface FileSaveResponse {
|
||||
message: string;
|
||||
data: FileLibrary;
|
||||
}
|
||||
|
||||
interface FileInfoResponse {
|
||||
message: string;
|
||||
data: FileLibrary;
|
||||
}
|
||||
|
||||
interface FileDeleteResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
interface FileCategoryListResponse {
|
||||
data: FileCategory[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
interface FileCategoryCreateResponse {
|
||||
message: string;
|
||||
data: FileCategory;
|
||||
}
|
||||
|
||||
interface FileCategoryUpdateResponse {
|
||||
message: string;
|
||||
data: FileCategory;
|
||||
}
|
||||
|
||||
interface FileCategoryDeleteResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 文件API接口定义
|
||||
export const FileAPI = {
|
||||
// 获取文件上传策略
|
||||
getUploadPolicy: async (filename: string, prefix: string = 'uploads/', maxSize: number = 10 * 1024 * 1024): Promise<FileUploadPolicyResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/upload/policy`, {
|
||||
params: { filename, prefix, maxSize }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 保存文件信息
|
||||
saveFileInfo: async (fileData: Partial<FileLibrary>): Promise<FileSaveResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/upload/save`, fileData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件列表
|
||||
getFileList: async (params?: {
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
category_id?: number,
|
||||
fileType?: string,
|
||||
keyword?: string
|
||||
}): Promise<FileListResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/upload/list`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个文件信息
|
||||
getFileInfo: async (id: number): Promise<FileInfoResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/upload/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新文件下载计数
|
||||
updateDownloadCount: async (id: number): Promise<FileDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/upload/${id}/download`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除文件
|
||||
deleteFile: async (id: number): Promise<FileDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/upload/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件分类列表
|
||||
getCategories: async (params?: {
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
search?: string
|
||||
}): Promise<FileCategoryListResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/file-categories`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 创建文件分类
|
||||
createCategory: async (data: Partial<FileCategory>): Promise<FileCategoryCreateResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/file-categories`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新文件分类
|
||||
updateCategory: async (id: number, data: Partial<FileCategory>): Promise<FileCategoryUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/file-categories/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除文件分类
|
||||
deleteCategory: async (id: number): Promise<FileCategoryDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/file-categories/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Theme API 响应类型
|
||||
export interface ThemeSettingsResponse {
|
||||
message: string;
|
||||
data: ThemeSettings;
|
||||
}
|
||||
|
||||
// Theme API 定义
|
||||
export const ThemeAPI = {
|
||||
// 获取主题设置
|
||||
getThemeSettings: async (): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/theme`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新主题设置
|
||||
updateThemeSettings: async (themeData: Partial<ThemeSettings>): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/theme`, themeData);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 重置主题设置
|
||||
resetThemeSettings: async (): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/theme/reset`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 图表数据API接口类型
|
||||
interface ChartDataResponse<T> {
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface UserActivityData {
|
||||
date: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface FileUploadsData {
|
||||
month: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface FileTypesData {
|
||||
type: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface DashboardOverviewData {
|
||||
userCount: number;
|
||||
fileCount: number;
|
||||
articleCount: number;
|
||||
todayLoginCount: number;
|
||||
}
|
||||
|
||||
// 图表数据API
|
||||
export const ChartAPI = {
|
||||
// 获取用户活跃度数据
|
||||
getUserActivity: async (): Promise<ChartDataResponse<UserActivityData[]>> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/charts/user-activity`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件上传统计数据
|
||||
getFileUploads: async (): Promise<ChartDataResponse<FileUploadsData[]>> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/charts/file-uploads`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件类型分布数据
|
||||
getFileTypes: async (): Promise<ChartDataResponse<FileTypesData[]>> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/charts/file-types`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取仪表盘概览数据
|
||||
getDashboardOverview: async (): Promise<ChartDataResponse<DashboardOverviewData>> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/charts/dashboard-overview`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 消息API接口类型
|
||||
interface MessagesResponse {
|
||||
data: UserMessage[];
|
||||
pagination: {
|
||||
total: number;
|
||||
current: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface MessageResponse {
|
||||
data: Message;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface MessageCountResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
// 消息API
|
||||
export const MessageAPI = {
|
||||
// 获取消息列表
|
||||
getMessages: async (params?: {
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
type?: string,
|
||||
status?: string,
|
||||
search?: string
|
||||
}): Promise<MessagesResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/messages`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage: async (data: {
|
||||
title: string,
|
||||
content: string,
|
||||
type: string,
|
||||
receiver_ids: number[]
|
||||
}): Promise<MessageResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/messages`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取未读消息数
|
||||
getUnreadCount: async (): Promise<MessageCountResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/messages/count/unread`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 标记消息为已读
|
||||
markAsRead: async (id: number): Promise<MessageResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/messages/${id}/read`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除消息
|
||||
deleteMessage: async (id: number): Promise<MessageResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/messages/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 地图相关API的接口类型定义
|
||||
export interface LoginLocationResponse {
|
||||
message: string;
|
||||
data: LoginLocation[];
|
||||
}
|
||||
|
||||
export interface LoginLocationDetailResponse {
|
||||
message: string;
|
||||
data: LoginLocationDetail;
|
||||
}
|
||||
|
||||
export interface LoginLocationUpdateResponse {
|
||||
message: string;
|
||||
data: LoginLocationDetail;
|
||||
}
|
||||
|
||||
// 知识库相关接口类型定义
|
||||
export interface KnowInfoListResponse {
|
||||
data: KnowInfo[];
|
||||
pagination: {
|
||||
total: number;
|
||||
current: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface KnowInfoResponse {
|
||||
data: KnowInfo;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface KnowInfoCreateResponse {
|
||||
message: string;
|
||||
data: KnowInfo;
|
||||
}
|
||||
|
||||
interface KnowInfoUpdateResponse {
|
||||
message: string;
|
||||
data: KnowInfo;
|
||||
}
|
||||
|
||||
interface KnowInfoDeleteResponse {
|
||||
message: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
|
||||
// 地图相关API
|
||||
export const MapAPI = {
|
||||
// 获取地图标记点数据
|
||||
getMarkers: async (params?: {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
userId?: number
|
||||
}): Promise<LoginLocationResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/markers`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取登录位置详情
|
||||
getLocationDetail: async (locationId: number): Promise<LoginLocationDetailResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/location/${locationId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新登录位置信息
|
||||
updateLocation: async (locationId: number, data: {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
location_name?: string;
|
||||
}): Promise<LoginLocationUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/map/location/${locationId}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 系统设置API
|
||||
// 知识库API
|
||||
export const KnowInfoAPI = {
|
||||
// 获取知识库列表
|
||||
getKnowInfos: async (params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
title?: string;
|
||||
category?: string;
|
||||
tags?: string;
|
||||
}): Promise<KnowInfoListResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/know-infos`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个知识详情
|
||||
getKnowInfo: async (id: number): Promise<KnowInfoResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/know-infos/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 创建知识
|
||||
createKnowInfo: async (data: Partial<KnowInfo>): Promise<KnowInfoCreateResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/know-infos`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新知识
|
||||
updateKnowInfo: async (id: number, data: Partial<KnowInfo>): Promise<KnowInfoUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/know-infos/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除知识
|
||||
deleteKnowInfo: async (id: number): Promise<KnowInfoDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/know-infos/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SystemAPI = {
|
||||
// 获取所有系统设置
|
||||
getSettings: async (): Promise<SystemSettingGroupData[]> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/settings`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取指定分组的系统设置
|
||||
getSettingsByGroup: async (group: string): Promise<SystemSetting[]> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/settings/group/${group}`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新系统设置
|
||||
updateSettings: async (settings: Partial<SystemSetting>[]): Promise<SystemSetting[]> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/settings`, settings);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 重置系统设置
|
||||
resetSettings: async (): Promise<SystemSetting[]> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/settings/reset`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
104
client/admin/api/auth.ts
Normal file
104
client/admin/api/auth.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import axios from 'axios';
|
||||
import type { User } from '../../share/types.ts';
|
||||
|
||||
interface AuthLoginResponse {
|
||||
message: string;
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
message: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AuthAPIType {
|
||||
login: (username: string, password: string, latitude?: number, longitude?: number) => Promise<AuthLoginResponse>;
|
||||
register: (username: string, email: string, password: string) => Promise<AuthResponse>;
|
||||
logout: () => Promise<AuthResponse>;
|
||||
getCurrentUser: () => Promise<User>;
|
||||
updateUser: (userId: number, userData: Partial<User>) => Promise<User>;
|
||||
changePassword: (oldPassword: string, newPassword: string) => Promise<AuthResponse>;
|
||||
requestPasswordReset: (email: string) => Promise<AuthResponse>;
|
||||
resetPassword: (token: string, newPassword: string) => Promise<AuthResponse>;
|
||||
}
|
||||
|
||||
export const AuthAPI: AuthAPIType = {
|
||||
login: async (username: string, password: string, latitude?: number, longitude?: number) => {
|
||||
try {
|
||||
const response = await axios.post('/auth/login', {
|
||||
username,
|
||||
password,
|
||||
latitude,
|
||||
longitude
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
register: async (username: string, email: string, password: string) => {
|
||||
try {
|
||||
const response = await axios.post('/auth/register', { username, email, password });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
const response = await axios.post('/auth/logout');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentUser: async () => {
|
||||
try {
|
||||
const response = await axios.get('/auth/me');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateUser: async (userId: number, userData: Partial<User>) => {
|
||||
try {
|
||||
const response = await axios.put(`/auth/users/${userId}`, userData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
changePassword: async (oldPassword: string, newPassword: string) => {
|
||||
try {
|
||||
const response = await axios.post('/auth/change-password', { oldPassword, newPassword });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
requestPasswordReset: async (email: string) => {
|
||||
try {
|
||||
const response = await axios.post('/auth/request-password-reset', { email });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
resetPassword: async (token: string, newPassword: string) => {
|
||||
try {
|
||||
const response = await axios.post('/auth/reset-password', { token, newPassword });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
66
client/admin/api/charts.ts
Normal file
66
client/admin/api/charts.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import axios from 'axios';
|
||||
|
||||
interface ChartDataResponse<T> {
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface UserActivityData {
|
||||
date: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface FileUploadsData {
|
||||
month: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface FileTypesData {
|
||||
type: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface DashboardOverviewData {
|
||||
userCount: number;
|
||||
fileCount: number;
|
||||
articleCount: number;
|
||||
todayLoginCount: number;
|
||||
}
|
||||
|
||||
export const ChartAPI = {
|
||||
getUserActivity: async (): Promise<ChartDataResponse<UserActivityData[]>> => {
|
||||
try {
|
||||
const response = await axios.get('/charts/user-activity');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getFileUploads: async (): Promise<ChartDataResponse<FileUploadsData[]>> => {
|
||||
try {
|
||||
const response = await axios.get('/charts/file-uploads');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getFileTypes: async (): Promise<ChartDataResponse<FileTypesData[]>> => {
|
||||
try {
|
||||
const response = await axios.get('/charts/file-types');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getDashboardOverview: async (): Promise<ChartDataResponse<DashboardOverviewData>> => {
|
||||
try {
|
||||
const response = await axios.get('/charts/dashboard-overview');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
159
client/admin/api/files.ts
Normal file
159
client/admin/api/files.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import axios from 'axios';
|
||||
import type { FileLibrary, FileCategory } from '../../share/types.ts';
|
||||
import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
|
||||
|
||||
interface FileUploadPolicyResponse {
|
||||
message: string;
|
||||
data: MinioUploadPolicy | OSSUploadPolicy;
|
||||
}
|
||||
|
||||
interface FileListResponse {
|
||||
message: string;
|
||||
data: {
|
||||
list: FileLibrary[];
|
||||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface FileSaveResponse {
|
||||
message: string;
|
||||
data: FileLibrary;
|
||||
}
|
||||
|
||||
interface FileInfoResponse {
|
||||
message: string;
|
||||
data: FileLibrary;
|
||||
}
|
||||
|
||||
interface FileDeleteResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface FileCategoryListResponse {
|
||||
data: FileCategory[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
interface FileCategoryCreateResponse {
|
||||
message: string;
|
||||
data: FileCategory;
|
||||
}
|
||||
|
||||
interface FileCategoryUpdateResponse {
|
||||
message: string;
|
||||
data: FileCategory;
|
||||
}
|
||||
|
||||
interface FileCategoryDeleteResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const FileAPI = {
|
||||
getUploadPolicy: async (filename: string, prefix: string = 'uploads/', maxSize: number = 10 * 1024 * 1024): Promise<FileUploadPolicyResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/upload/policy', {
|
||||
params: { filename, prefix, maxSize }
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
saveFileInfo: async (fileData: Partial<FileLibrary>): Promise<FileSaveResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/upload/save', fileData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getFileList: async (params?: {
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
category_id?: number,
|
||||
fileType?: string,
|
||||
keyword?: string
|
||||
}): Promise<FileListResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/upload/list', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getFileInfo: async (id: number): Promise<FileInfoResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/upload/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateDownloadCount: async (id: number): Promise<FileDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/upload/${id}/download`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteFile: async (id: number): Promise<FileDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/upload/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getCategories: async (params?: {
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
search?: string
|
||||
}): Promise<FileCategoryListResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/file-categories', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
createCategory: async (data: Partial<FileCategory>): Promise<FileCategoryCreateResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/file-categories', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateCategory: async (id: number, data: Partial<FileCategory>): Promise<FileCategoryUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/file-categories/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteCategory: async (id: number): Promise<FileCategoryDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/file-categories/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
25
client/admin/api/index.ts
Normal file
25
client/admin/api/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// 基础配置
|
||||
export const API_BASE_URL = '/api';
|
||||
// 全局axios配置
|
||||
axios.defaults.baseURL = API_BASE_URL;
|
||||
|
||||
// 获取OSS完整URL
|
||||
export const getOssUrl = (path: string): string => {
|
||||
// 获取全局配置中的OSS_HOST,如果不存在使用默认值
|
||||
const ossHost = (window.CONFIG?.OSS_BASE_URL) || '';
|
||||
// 确保path不以/开头
|
||||
const ossPath = path.startsWith('/') ? path.substring(1) : path;
|
||||
return `${ossHost}/${ossPath}`;
|
||||
};
|
||||
|
||||
export * from './auth.ts';
|
||||
export * from './users.ts';
|
||||
export * from './files.ts';
|
||||
export * from './theme.ts';
|
||||
export * from './charts.ts';
|
||||
export * from './messages.ts';
|
||||
export * from './sys.ts';
|
||||
export * from './know_info.ts';
|
||||
export * from './maps.ts';
|
||||
92
client/admin/api/know_info.ts
Normal file
92
client/admin/api/know_info.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import axios from 'axios';
|
||||
import type { KnowInfo } from '../../share/types.ts';
|
||||
|
||||
export interface KnowInfoListResponse {
|
||||
data: KnowInfo[];
|
||||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface KnowInfoResponse {
|
||||
data: KnowInfo;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface KnowInfoCreateResponse {
|
||||
message: string;
|
||||
data: KnowInfo;
|
||||
}
|
||||
|
||||
interface KnowInfoUpdateResponse {
|
||||
message: string;
|
||||
data: KnowInfo;
|
||||
}
|
||||
|
||||
interface KnowInfoDeleteResponse {
|
||||
message: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
|
||||
// 知识库API
|
||||
export const KnowInfoAPI = {
|
||||
// 获取知识库列表
|
||||
getKnowInfos: async (params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
title?: string;
|
||||
category?: string;
|
||||
tags?: string;
|
||||
}): Promise<KnowInfoListResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/know-infos', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个知识详情
|
||||
getKnowInfo: async (id: number): Promise<KnowInfoResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/know-infos/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 创建知识
|
||||
createKnowInfo: async (data: Partial<KnowInfo>): Promise<KnowInfoCreateResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/know-infos', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新知识
|
||||
updateKnowInfo: async (id: number, data: Partial<KnowInfo>): Promise<KnowInfoUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/know-infos/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除知识
|
||||
deleteKnowInfo: async (id: number): Promise<KnowInfoDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/know-infos/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
63
client/admin/api/maps.ts
Normal file
63
client/admin/api/maps.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import axios from 'axios';
|
||||
import { API_BASE_URL } from './index.ts';
|
||||
import type {
|
||||
LoginLocation, LoginLocationDetail,
|
||||
} from '../../share/types.ts';
|
||||
|
||||
|
||||
// 地图相关API的接口类型定义
|
||||
export interface LoginLocationResponse {
|
||||
message: string;
|
||||
data: LoginLocation[];
|
||||
}
|
||||
|
||||
export interface LoginLocationDetailResponse {
|
||||
message: string;
|
||||
data: LoginLocationDetail;
|
||||
}
|
||||
|
||||
export interface LoginLocationUpdateResponse {
|
||||
message: string;
|
||||
data: LoginLocationDetail;
|
||||
}
|
||||
|
||||
// 地图相关API
|
||||
export const MapAPI = {
|
||||
// 获取地图标记点数据
|
||||
getMarkers: async (params?: {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
userId?: number
|
||||
}): Promise<LoginLocationResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/markers`, { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取登录位置详情
|
||||
getLocationDetail: async (locationId: number): Promise<LoginLocationDetailResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/location/${locationId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新登录位置信息
|
||||
updateLocation: async (locationId: number, data: {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
location_name?: string;
|
||||
}): Promise<LoginLocationUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/map/location/${locationId}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
79
client/admin/api/messages.ts
Normal file
79
client/admin/api/messages.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import type { UserMessage, Message } from '../../share/types.ts';
|
||||
|
||||
interface MessagesResponse {
|
||||
data: UserMessage[];
|
||||
pagination: {
|
||||
total: number;
|
||||
current: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface MessageResponse {
|
||||
data: Message;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface MessageCountResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const MessageAPI = {
|
||||
getMessages: async (params?: {
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
type?: string,
|
||||
status?: string,
|
||||
search?: string
|
||||
}): Promise<MessagesResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/messages', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (data: {
|
||||
title: string,
|
||||
content: string,
|
||||
type: string,
|
||||
receiver_ids: number[]
|
||||
}): Promise<MessageResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/messages', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getUnreadCount: async (): Promise<MessageCountResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/messages/count/unread');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
markAsRead: async (id: number): Promise<MessageResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/messages/${id}/read`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteMessage: async (id: number): Promise<MessageResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/messages/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
47
client/admin/api/sys.ts
Normal file
47
client/admin/api/sys.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import type {
|
||||
SystemSetting, SystemSettingGroupData,
|
||||
} from '../../share/types.ts';
|
||||
|
||||
export const SystemAPI = {
|
||||
// 获取所有系统设置
|
||||
getSettings: async (): Promise<SystemSettingGroupData[]> => {
|
||||
try {
|
||||
const response = await axios.get('/settings');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取指定分组的系统设置
|
||||
getSettingsByGroup: async (group: string): Promise<SystemSetting[]> => {
|
||||
try {
|
||||
const response = await axios.get(`/settings/group/${group}`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新系统设置
|
||||
updateSettings: async (settings: Partial<SystemSetting>[]): Promise<SystemSetting[]> => {
|
||||
try {
|
||||
const response = await axios.put('/settings', settings);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 重置系统设置
|
||||
resetSettings: async (): Promise<SystemSetting[]> => {
|
||||
try {
|
||||
const response = await axios.post('/settings/reset');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
36
client/admin/api/theme.ts
Normal file
36
client/admin/api/theme.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import axios from 'axios';
|
||||
import type { ThemeSettings } from '../../share/types.ts';
|
||||
|
||||
export interface ThemeSettingsResponse {
|
||||
message: string;
|
||||
data: ThemeSettings;
|
||||
}
|
||||
|
||||
export const ThemeAPI = {
|
||||
getThemeSettings: async (): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await axios.get('/theme');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateThemeSettings: async (themeData: Partial<ThemeSettings>): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await axios.put('/theme', themeData);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
resetThemeSettings: async (): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await axios.post('/theme/reset');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
79
client/admin/api/users.ts
Normal file
79
client/admin/api/users.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import type { User } from '../../share/types.ts';
|
||||
|
||||
interface UsersResponse {
|
||||
data: User[];
|
||||
pagination: {
|
||||
total: number;
|
||||
current: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
data: User;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface UserCreateResponse {
|
||||
message: string;
|
||||
data: User;
|
||||
}
|
||||
|
||||
interface UserUpdateResponse {
|
||||
message: string;
|
||||
data: User;
|
||||
}
|
||||
|
||||
interface UserDeleteResponse {
|
||||
message: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const UserAPI = {
|
||||
getUsers: async (params?: { page?: number, limit?: number, search?: string }): Promise<UsersResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/users', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getUser: async (userId: number): Promise<UserResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
createUser: async (userData: Partial<User>): Promise<UserCreateResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/users', userData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateUser: async (userId: number, userData: Partial<User>): Promise<UserUpdateResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/users/${userId}`, userData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteUser: async (userId: number): Promise<UserDeleteResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { OssType } from '../share/types.ts';
|
||||
|
||||
import { FileAPI } from './api.ts';
|
||||
import { FileAPI } from './api/index.ts';
|
||||
|
||||
// MinIO文件上传组件
|
||||
export const Uploader = ({
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {
|
||||
AuthAPI,
|
||||
ThemeAPI
|
||||
} from './api.ts';
|
||||
} from './api/index.ts';
|
||||
|
||||
|
||||
// 配置 dayjs 插件
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Layout, Menu, Button, Table, Space,
|
||||
Form, Input, Select, message, Modal,
|
||||
Card, Spin, Row, Col, Breadcrumb, Avatar,
|
||||
Dropdown, ConfigProvider, theme, Typography,
|
||||
Switch, Badge, Image, Upload, Divider, Descriptions,
|
||||
Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
|
||||
Card, Spin, Row, Col, Statistic,
|
||||
} from 'antd';
|
||||
|
||||
import {
|
||||
@@ -15,13 +10,10 @@ import { Line , Pie, Column} from "@ant-design/plots";
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
|
||||
import { ChartAPI } from './api.ts';
|
||||
import { ChartAPI } from './api/index.ts';
|
||||
import { useTheme } from './hooks_sys.tsx';
|
||||
|
||||
interface ChartTooltipInfo {
|
||||
items: Array<Record<string, any>>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
|
||||
// 用户活跃度图表组件
|
||||
const UserActivityChart: React.FC = () => {
|
||||
@@ -49,7 +41,7 @@ const UserActivityChart: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="用户活跃度趋势" bordered={false}>
|
||||
<Card title="用户活跃度趋势" variant="borderless">
|
||||
<Line {...config} />
|
||||
</Card>
|
||||
);
|
||||
@@ -92,7 +84,7 @@ const FileUploadsChart: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="文件上传统计" bordered={false}>
|
||||
<Card title="文件上传统计" variant="borderless">
|
||||
<Column {...config} />
|
||||
</Card>
|
||||
);
|
||||
@@ -130,7 +122,7 @@ const FileTypesChart: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="文件类型分布" bordered={false}>
|
||||
<Card title="文件类型分布" variant="borderless">
|
||||
<Pie {...config} />
|
||||
</Card>
|
||||
);
|
||||
@@ -151,7 +143,7 @@ const DashboardOverview: React.FC = () => {
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card bordered={false}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="用户总数"
|
||||
value={overviewData?.userCount || 0}
|
||||
@@ -160,7 +152,7 @@ const DashboardOverview: React.FC = () => {
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card bordered={false}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="文件总数"
|
||||
value={overviewData?.fileCount || 0}
|
||||
@@ -169,7 +161,7 @@ const DashboardOverview: React.FC = () => {
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card bordered={false}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="文章总数"
|
||||
value={overviewData?.articleCount || 0}
|
||||
@@ -178,7 +170,7 @@ const DashboardOverview: React.FC = () => {
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card bordered={false}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="今日登录"
|
||||
value={overviewData?.todayLoginCount || 0}
|
||||
|
||||
44
client/admin/pages_dashboard.tsx
Normal file
44
client/admin/pages_dashboard.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card, Row, Col, Typography, Statistic
|
||||
} from 'antd';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
// 仪表盘页面
|
||||
export const DashboardPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Title level={2}>仪表盘</Title>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="活跃用户"
|
||||
value={112893}
|
||||
loading={false}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="系统消息"
|
||||
value={93}
|
||||
loading={false}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="在线用户"
|
||||
value={1128}
|
||||
loading={false}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Layout, Menu, Button, Table, Space,
|
||||
Form, Input, Select, message, Modal,
|
||||
Card, Spin, Row, Col, Breadcrumb, Avatar,
|
||||
Dropdown, ConfigProvider, theme, Typography,
|
||||
Switch, Badge, Image, Upload, Divider, Descriptions,
|
||||
Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
|
||||
Button, Table, Space, Form, Input, Select,
|
||||
message, Modal, Card, Typography, Tag, Popconfirm,
|
||||
Tabs, Image, Upload, Descriptions
|
||||
} from 'antd';
|
||||
import {
|
||||
UploadOutlined,
|
||||
@@ -14,341 +11,17 @@ import {
|
||||
FileWordOutlined,
|
||||
FilePdfOutlined,
|
||||
FileOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
} from '@ant-design/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import { uploadMinIOWithPolicy,uploadOSSWithPolicy } from '@d8d-appcontainer/api';
|
||||
import { uploadMinIOWithPolicy, uploadOSSWithPolicy } from '@d8d-appcontainer/api';
|
||||
import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import type {
|
||||
FileLibrary, FileCategory, KnowInfo
|
||||
} from '../share/types.ts';
|
||||
|
||||
import {
|
||||
AuditStatus,AuditStatusNameMap,
|
||||
OssType,
|
||||
} from '../share/types.ts';
|
||||
|
||||
import { getEnumOptions } from './utils.ts';
|
||||
|
||||
import {
|
||||
FileAPI,
|
||||
UserAPI,
|
||||
} from './api.ts';
|
||||
|
||||
|
||||
// 配置 dayjs 插件
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
|
||||
// 设置 dayjs 语言
|
||||
dayjs.locale('zh-cn');
|
||||
import { FileAPI } from './api/index.ts';
|
||||
import type { FileLibrary, FileCategory } from '../share/types.ts';
|
||||
import { OssType } from '../share/types.ts';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
||||
// 仪表盘页面
|
||||
export const DashboardPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Title level={2}>仪表盘</Title>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="活跃用户"
|
||||
value={112893}
|
||||
loading={false}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="系统消息"
|
||||
value={93}
|
||||
loading={false}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="在线用户"
|
||||
value={1128}
|
||||
loading={false}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 用户管理页面
|
||||
export const UsersPage = () => {
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
search: ''
|
||||
});
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [editingUser, setEditingUser] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { data: usersData, isLoading, refetch } = useQuery({
|
||||
queryKey: ['users', searchParams],
|
||||
queryFn: async () => {
|
||||
return await UserAPI.getUsers(searchParams);
|
||||
}
|
||||
});
|
||||
|
||||
const users = usersData?.data || [];
|
||||
const pagination = {
|
||||
current: searchParams.page,
|
||||
pageSize: searchParams.limit,
|
||||
total: usersData?.pagination?.total || 0
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (values: any) => {
|
||||
setSearchParams(prev => ({
|
||||
...prev,
|
||||
search: values.search || '',
|
||||
page: 1
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理分页变化
|
||||
const handleTableChange = (newPagination: any) => {
|
||||
setSearchParams(prev => ({
|
||||
...prev,
|
||||
page: newPagination.current,
|
||||
limit: newPagination.pageSize
|
||||
}));
|
||||
};
|
||||
|
||||
// 打开创建用户模态框
|
||||
const showCreateModal = () => {
|
||||
setModalTitle('创建用户');
|
||||
setEditingUser(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 打开编辑用户模态框
|
||||
const showEditModal = (user: any) => {
|
||||
setModalTitle('编辑用户');
|
||||
setEditingUser(user);
|
||||
form.setFieldsValue(user);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理模态框确认
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (editingUser) {
|
||||
// 编辑用户
|
||||
await UserAPI.updateUser(editingUser.id, values);
|
||||
message.success('用户更新成功');
|
||||
} else {
|
||||
// 创建用户
|
||||
await UserAPI.createUser(values);
|
||||
message.success('用户创建成功');
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
refetch(); // 刷新用户列表
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error);
|
||||
message.error('操作失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除用户
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await UserAPI.deleteUser(id);
|
||||
message.success('用户删除成功');
|
||||
refetch(); // 刷新用户列表
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error);
|
||||
message.error('删除失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: string) => (
|
||||
<Tag color={role === 'admin' ? 'red' : 'blue'}>
|
||||
{role === 'admin' ? '管理员' : '普通用户'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="middle">
|
||||
<Button type="link" onClick={() => showEditModal(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除此用户吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={2}>用户管理</Title>
|
||||
<Card>
|
||||
<Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
|
||||
<Form.Item name="search" label="搜索">
|
||||
<Input placeholder="用户名/昵称/邮箱" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
搜索
|
||||
</Button>
|
||||
<Button type="primary" onClick={showCreateModal}>
|
||||
创建用户
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 创建/编辑用户模态框 */}
|
||||
<Modal
|
||||
title={modalTitle}
|
||||
open={modalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
}}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="nickname"
|
||||
label="昵称"
|
||||
rules={[{ required: true, message: '请输入昵称' }]}
|
||||
>
|
||||
<Input placeholder="请输入昵称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
|
||||
{!editingUser && (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' }
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="角色"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select placeholder="请选择角色">
|
||||
<Select.Option value="user">普通用户</Select.Option>
|
||||
<Select.Option value="admin">管理员</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 文件库管理页面
|
||||
export const FileLibraryPage = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -1,543 +0,0 @@
|
||||
import { JSDOM } from 'jsdom'
|
||||
import React from 'react'
|
||||
import {render, waitFor, within, fireEvent} from '@testing-library/react'
|
||||
import {userEvent} from '@testing-library/user-event'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router'
|
||||
import {
|
||||
assertEquals,
|
||||
assertExists,
|
||||
assertNotEquals,
|
||||
assertRejects,
|
||||
assert,
|
||||
} from "https://deno.land/std@0.217.0/assert/mod.ts";
|
||||
import axios from 'axios';
|
||||
import { KnowInfoPage } from "./pages_know_info.tsx"
|
||||
import { AuthProvider } from './hooks_sys.tsx'
|
||||
import { ProtectedRoute } from './components_protected_route.tsx'
|
||||
|
||||
// 拦截React DOM中的attachEvent和detachEvent错误
|
||||
const originalError = console.error;
|
||||
console.error = (...args) => {
|
||||
// 过滤掉attachEvent和detachEvent相关的错误
|
||||
if (args[0] instanceof Error) {
|
||||
if (args[0].message?.includes('attachEvent is not a function') ||
|
||||
args[0].message?.includes('detachEvent is not a function')) {
|
||||
return; // 不输出这些错误
|
||||
}
|
||||
} else if (typeof args[0] === 'string') {
|
||||
if (args[0].includes('attachEvent is not a function') ||
|
||||
args[0].includes('detachEvent is not a function')) {
|
||||
return; // 不输出这些错误
|
||||
}
|
||||
}
|
||||
originalError(...args);
|
||||
};
|
||||
|
||||
// 应用入口组件
|
||||
const App = () => {
|
||||
// 路由配置
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<KnowInfoPage />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
},
|
||||
]);
|
||||
return <RouterProvider router={router} />
|
||||
};
|
||||
// setup function
|
||||
function setup() {
|
||||
|
||||
const dom = new JSDOM(`<body></body>`, {
|
||||
runScripts: "dangerously",
|
||||
pretendToBeVisual: true,
|
||||
url: "http://localhost",
|
||||
});
|
||||
|
||||
// 模拟浏览器环境
|
||||
globalThis.window = dom.window;
|
||||
globalThis.document = dom.window.document;
|
||||
|
||||
// 添加必要的 DOM 配置
|
||||
globalThis.Node = dom.window.Node;
|
||||
globalThis.Document = dom.window.Document;
|
||||
globalThis.HTMLInputElement = dom.window.HTMLInputElement;
|
||||
globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
|
||||
|
||||
// 定义浏览器环境所需的类
|
||||
globalThis.Element = dom.window.Element;
|
||||
globalThis.HTMLElement = dom.window.HTMLElement;
|
||||
globalThis.ShadowRoot = dom.window.ShadowRoot;
|
||||
globalThis.SVGElement = dom.window.SVGElement;
|
||||
|
||||
|
||||
|
||||
// 模拟 getComputedStyle
|
||||
globalThis.getComputedStyle = (elt) => {
|
||||
const style = new dom.window.CSSStyleDeclaration();
|
||||
style.getPropertyValue = () => '';
|
||||
return style;
|
||||
};
|
||||
|
||||
// 模拟matchMedia函数
|
||||
globalThis.matchMedia = (query) => ({
|
||||
matches: query.includes('max-width'),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
});
|
||||
|
||||
// 模拟动画相关API
|
||||
globalThis.AnimationEvent = globalThis.AnimationEvent || dom.window.Event;
|
||||
globalThis.TransitionEvent = globalThis.TransitionEvent || dom.window.Event;
|
||||
|
||||
// 模拟requestAnimationFrame
|
||||
globalThis.requestAnimationFrame = globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 0));
|
||||
globalThis.cancelAnimationFrame = globalThis.cancelAnimationFrame || clearTimeout;
|
||||
|
||||
// 设置浏览器尺寸相关方法
|
||||
window.resizeTo = (width, height) => {
|
||||
window.innerWidth = width || window.innerWidth;
|
||||
window.innerHeight = height || window.innerHeight;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
};
|
||||
window.scrollTo = () => {};
|
||||
|
||||
|
||||
const customScreen = within(document.body);
|
||||
|
||||
const user = userEvent.setup({
|
||||
document: dom.window.document,
|
||||
delay: 10,
|
||||
skipAutoClose: true,
|
||||
});
|
||||
|
||||
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsInNlc3Npb25JZCI6Ijk4T2lzTW5SMm0zQ0dtNmo4SVZrNyIsInJvbGVJbmZvIjpudWxsLCJpYXQiOjE3NDQzNjIzNTUsImV4cCI6MTc0NDQ0ODc1NX0.k1Ld7qWAZmdzsbjmrl_0ec1FqF_GimaOuQIic4znRtc');
|
||||
|
||||
axios.defaults.baseURL = 'https://23957.dev.d8dcloud.com'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
return {
|
||||
user,
|
||||
// Import `render` from the framework library of your choice.
|
||||
// See https://testing-library.com/docs/dom-testing-library/install#wrappers
|
||||
...render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// // 使用异步测试处理组件渲染
|
||||
// Deno.test({
|
||||
// name: '知识库管理页面基础测试',
|
||||
// fn: async (t) => {
|
||||
// // 存储所有需要清理的定时器
|
||||
// const timers: number[] = [];
|
||||
// const originalSetTimeout = globalThis.setTimeout;
|
||||
// const originalSetInterval = globalThis.setInterval;
|
||||
|
||||
// // 重写定时器方法以跟踪所有创建的定时器
|
||||
// globalThis.setTimeout = ((callback, delay, ...args) => {
|
||||
// const id = originalSetTimeout(callback, delay, ...args);
|
||||
// timers.push(id);
|
||||
// return id;
|
||||
// }) as typeof setTimeout;
|
||||
|
||||
// globalThis.setInterval = ((callback, delay, ...args) => {
|
||||
// const id = originalSetInterval(callback, delay, ...args);
|
||||
// timers.push(id);
|
||||
// return id;
|
||||
// }) as typeof setInterval;
|
||||
|
||||
// // 清理函数
|
||||
// const cleanup = () => {
|
||||
// for (const id of timers) {
|
||||
// clearTimeout(id);
|
||||
// clearInterval(id);
|
||||
// }
|
||||
// // 恢复原始定时器方法
|
||||
// globalThis.setTimeout = originalSetTimeout;
|
||||
// globalThis.setInterval = originalSetInterval;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// try {
|
||||
|
||||
|
||||
// // // 渲染组件
|
||||
// // const {
|
||||
// // findByText, findByPlaceholderText, queryByText,
|
||||
// // findByRole, findAllByRole, findByLabelText, findAllByText, debug,
|
||||
// // queryByRole
|
||||
|
||||
// // } = render(
|
||||
// // <QueryClientProvider client={queryClient}>
|
||||
// // <AuthProvider>
|
||||
// // <App />
|
||||
// // </AuthProvider>
|
||||
// // </QueryClientProvider>
|
||||
// // );
|
||||
|
||||
// // 测试1: 基本渲染
|
||||
// await t.step('应正确渲染页面元素', async () => {
|
||||
// const { findByText } = setup()
|
||||
// await waitFor(async () => {
|
||||
// const title = await findByText(/知识库管理/i);
|
||||
// assertExists(title, '未找到知识库管理标题');
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// });
|
||||
// });
|
||||
|
||||
// // 初始加载表格数据
|
||||
// await t.step('初始加载表格数据', async () => {
|
||||
// const { findByRole } = setup()
|
||||
// await waitFor(async () => {
|
||||
// const table = await findByRole('table');
|
||||
// const rows = await within(table).findAllByRole('row');
|
||||
|
||||
// // 应该大于2行
|
||||
// assert(rows.length > 2, '表格没有数据'); // 1是表头行 2是数据行
|
||||
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// });
|
||||
// });
|
||||
|
||||
// // 测试2: 搜索表单功能
|
||||
// await t.step('搜索表单应正常工作', async () => {
|
||||
// const {findByPlaceholderText, findByText, findByRole, user} = setup()
|
||||
|
||||
// // 等待知识库管理标题出现
|
||||
// await waitFor(async () => {
|
||||
// const title = await findByText(/知识库管理/i);
|
||||
// assertExists(title, '未找到知识库管理标题');
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// });
|
||||
|
||||
// // 直接查找标题搜索输入框和搜索按钮
|
||||
// const searchInput = await findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement;
|
||||
// const searchButton = await findByText(/搜 索/i);
|
||||
|
||||
// assertExists(searchInput, '未找到搜索输入框');
|
||||
// assertExists(searchButton, '未找到搜索按钮');
|
||||
|
||||
// // 输入搜索内容
|
||||
// await user.type(searchInput, '数据分析')
|
||||
// assertEquals(searchInput.value, '数据分析', '搜索输入框值未更新');
|
||||
|
||||
// // 提交搜索
|
||||
// await user.click(searchButton);
|
||||
|
||||
|
||||
// let rows: HTMLElement[] = [];
|
||||
|
||||
|
||||
// const table = await findByRole('table');
|
||||
// assertExists(table, '未找到数据表格');
|
||||
|
||||
// // 等待表格刷新并验证
|
||||
// await waitFor(async () => {
|
||||
// rows = await within(table).findAllByRole('row');
|
||||
// assert(rows.length === 2, '表格未刷新');
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// onTimeout: () => new Error('等待表格刷新超时')
|
||||
// });
|
||||
|
||||
// // 等待搜索结果并验证
|
||||
// await waitFor(async () => {
|
||||
// rows = await within(table).findAllByRole('row');
|
||||
// assert(rows.length > 2, '表格没有数据');
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// onTimeout: () => new Error('等待搜索结果超时')
|
||||
// });
|
||||
|
||||
|
||||
|
||||
// // 检查至少有一行包含"数据分析"
|
||||
// const matchResults = await Promise.all(rows.map(async row => {
|
||||
// try{
|
||||
// const cells = await within(row).findAllByRole('cell');
|
||||
// return cells.some(cell => {
|
||||
// return cell.textContent?.includes('数据分析')
|
||||
// });
|
||||
// } catch (error: unknown) {
|
||||
// // console.error('搜索结果获取失败', error)
|
||||
// return false
|
||||
// }
|
||||
// }))
|
||||
// // console.log('matchResults', matchResults)
|
||||
// const hasMatch = matchResults.some(result => result);
|
||||
|
||||
// assert(hasMatch, '搜索结果中没有找到包含"数据分析"的文章');
|
||||
// });
|
||||
|
||||
// // 测试3: 表格数据加载
|
||||
// await t.step('表格应加载并显示数据', async () => {
|
||||
// const {findByRole, queryByText} = setup()
|
||||
// // 等待数据加载完成或表格出现,最多等待5秒
|
||||
// await waitFor(async () => {
|
||||
// // 检查加载状态是否消失
|
||||
// const loading = queryByText(/正在加载数据/i);
|
||||
// if (loading) {
|
||||
// throw new Error('数据仍在加载中');
|
||||
// }
|
||||
|
||||
// // 检查表格是否出现
|
||||
// const table = await findByRole('table');
|
||||
// assertExists(table, '未找到数据表格');
|
||||
|
||||
// // 检查表格是否有数据行
|
||||
// const rows = await within(table).findAllByRole('row');
|
||||
// assertNotEquals(rows.length, 1, '表格没有数据行'); // 1是表头行
|
||||
// }, {
|
||||
// timeout: 5000, // 5秒超时
|
||||
// onTimeout: (error) => {
|
||||
// return new Error(`数据加载超时: ${error.message}`);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// // 测试4: 添加文章功能
|
||||
// await t.step('应能打开添加文章模态框', async () => {
|
||||
// const {findByText, findByRole, user} = setup()
|
||||
// // 等待知识库管理标题出现
|
||||
// await waitFor(async () => {
|
||||
// const title = await findByText(/知识库管理/i);
|
||||
// assertExists(title, '未找到知识库管理标题');
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// });
|
||||
|
||||
// const addButton = await findByText(/添加文章/i);
|
||||
// assertExists(addButton, '未找到添加文章按钮');
|
||||
|
||||
// await user.click(addButton);
|
||||
|
||||
// // 找到模态框
|
||||
// const modal = await findByRole('dialog');
|
||||
// assertExists(modal, '未找到模态框');
|
||||
|
||||
// const modalTitle = await within(modal).findByText(/添加知识库文章/i);
|
||||
// assertExists(modalTitle, '未找到添加文章模态框');
|
||||
|
||||
// // 验证表单字段
|
||||
// const titleInput = await within(modal).findByPlaceholderText(/请输入文章标题/i);
|
||||
// assertExists(titleInput, '未找到标题输入框');
|
||||
|
||||
// const contentInput = await within(modal).findByPlaceholderText(/请输入文章内容/i);
|
||||
// assertExists(contentInput, '未找到文章内容输入框');
|
||||
|
||||
// // 取消
|
||||
// const cancelButton = await within(modal).findByText(/取 消/i);
|
||||
// assertExists(cancelButton, '未找到取消按钮');
|
||||
// await user.click(cancelButton);
|
||||
|
||||
// // 验证模态框是否关闭
|
||||
// await waitFor(async () => {
|
||||
|
||||
// const modalTitle = await within(modal).findByText(/添加知识库文章/i);
|
||||
// assertExists(!modalTitle, '模态框未关闭');
|
||||
// }, {
|
||||
// timeout: 1000 * 5,
|
||||
// onTimeout: () => new Error('等待模态框关闭超时')
|
||||
// });
|
||||
// });
|
||||
|
||||
// } finally {
|
||||
// // 确保清理所有定时器
|
||||
// cleanup();
|
||||
// }
|
||||
// },
|
||||
// sanitizeOps: false, // 禁用操作清理检查
|
||||
// sanitizeResources: false, // 禁用资源清理检查
|
||||
// });
|
||||
|
||||
Deno.test({
|
||||
name: '知识库管理页面新增测试',
|
||||
fn: async (t) => {
|
||||
// 存储所有需要清理的定时器
|
||||
const timers: number[] = [];
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const originalSetInterval = globalThis.setInterval;
|
||||
|
||||
// 重写定时器方法以跟踪所有创建的定时器
|
||||
globalThis.setTimeout = ((callback, delay, ...args) => {
|
||||
const id = originalSetTimeout(callback, delay, ...args);
|
||||
timers.push(id);
|
||||
return id;
|
||||
}) as typeof setTimeout;
|
||||
|
||||
globalThis.setInterval = ((callback, delay, ...args) => {
|
||||
const id = originalSetInterval(callback, delay, ...args);
|
||||
timers.push(id);
|
||||
return id;
|
||||
}) as typeof setInterval;
|
||||
|
||||
// 清理函数
|
||||
const cleanup = () => {
|
||||
for (const id of timers) {
|
||||
clearTimeout(id);
|
||||
clearInterval(id);
|
||||
}
|
||||
// 恢复原始定时器方法
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
globalThis.setInterval = originalSetInterval;
|
||||
};
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
|
||||
// // 渲染组件
|
||||
// const {
|
||||
// findByText, findByPlaceholderText, queryByText,
|
||||
// findByRole, findAllByRole, findByLabelText, findAllByText, debug,
|
||||
// queryByRole
|
||||
|
||||
// } = render(
|
||||
// <QueryClientProvider client={queryClient}>
|
||||
// <AuthProvider>
|
||||
// <App />
|
||||
// </AuthProvider>
|
||||
// </QueryClientProvider>
|
||||
// );
|
||||
|
||||
|
||||
// 测试5: 完整添加文章流程
|
||||
await t.step('应能完整添加一篇文章', async () => {
|
||||
const {findByText, findByRole, debug, user} = setup()
|
||||
// 等待知识库管理标题出现
|
||||
await waitFor(async () => {
|
||||
const title = await findByText(/知识库管理/i);
|
||||
assertExists(title, '未找到知识库管理标题');
|
||||
}, {
|
||||
timeout: 1000 * 5,
|
||||
});
|
||||
// 打开添加模态框
|
||||
const addButton = await findByText(/添加文章/i);
|
||||
assertExists(addButton, '未找到添加文章按钮');
|
||||
|
||||
await user.click(addButton);
|
||||
|
||||
// 找到模态框
|
||||
const modal = await findByRole('dialog');
|
||||
assertExists(modal, '未找到模态框');
|
||||
|
||||
const modalTitle = await within(modal).findByText(/添加知识库文章/i);
|
||||
assertExists(modalTitle, '未找到添加文章模态框');
|
||||
|
||||
// 确保上一个测试的输入框值不会影响这个测试
|
||||
// 在模态框内查找标题和内容输入框
|
||||
const titleInput = await within(modal).findByPlaceholderText(/请输入文章标题/i) as HTMLInputElement;
|
||||
const contentInput = await within(modal).findByPlaceholderText(/请输入文章内容/i) as HTMLTextAreaElement;
|
||||
const submitButton = await within(modal).findByText(/确 定/i);
|
||||
|
||||
assertExists(titleInput, '未找到模态框中的标题输入框');
|
||||
assertExists(contentInput, '未找到文章内容输入框');
|
||||
assertExists(submitButton, '未找到提交按钮');
|
||||
|
||||
// 先清空输入框,以防止之前的测试影响
|
||||
// await user.clear(titleInput);
|
||||
// await user.clear(contentInput);
|
||||
|
||||
// 输入新值
|
||||
await user.type(titleInput, '测试文章标题');
|
||||
await user.type(contentInput, '这是测试文章内容');
|
||||
|
||||
debug(titleInput);
|
||||
debug(contentInput);
|
||||
debug(submitButton);
|
||||
|
||||
// 提交表单
|
||||
await user.click(submitButton);
|
||||
|
||||
// 验证表单字段
|
||||
assertEquals(titleInput.value, '测试文章标题', '模态框中标题输入框值未更新');
|
||||
assertEquals(contentInput.value, '这是测试文章内容', '内容输入框值未更新');
|
||||
|
||||
let rows: HTMLElement[] = [];
|
||||
|
||||
|
||||
const table = await findByRole('table');
|
||||
assertExists(table, '未找到数据表格');
|
||||
|
||||
// 等待表格刷新并验证
|
||||
await waitFor(async () => {
|
||||
rows = await within(table).findAllByRole('row');
|
||||
assert(rows.length === 2, '表格未刷新');
|
||||
}, {
|
||||
timeout: 1000 * 5,
|
||||
onTimeout: () => new Error('等待表格刷新超时')
|
||||
});
|
||||
|
||||
// 等待搜索结果并验证
|
||||
await waitFor(async () => {
|
||||
rows = await within(table).findAllByRole('row');
|
||||
assert(rows.length > 2, '表格没有数据');
|
||||
}, {
|
||||
timeout: 1000 * 5,
|
||||
onTimeout: () => new Error('等待搜索结果超时')
|
||||
});
|
||||
|
||||
// 检查至少有一行包含"测试文章标题"
|
||||
const matchResults = await Promise.all(rows.map(async row => {
|
||||
try{
|
||||
const cells = await within(row).findAllByRole('cell');
|
||||
return cells.some(cell => {
|
||||
return cell.textContent?.includes('测试文章标题')
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// console.error('搜索结果获取失败', error)
|
||||
return false
|
||||
}
|
||||
}))
|
||||
// console.log('matchResults', matchResults)
|
||||
const hasMatch = matchResults.some(result => result);
|
||||
|
||||
assert(hasMatch, '搜索结果中没有找到包含"测试文章标题"的文章');
|
||||
});
|
||||
|
||||
// // 测试5: 分页功能
|
||||
// await t.step('应显示分页控件', async () => {
|
||||
// const pagination = await findByRole('navigation');
|
||||
// assertExists(pagination, '未找到分页控件');
|
||||
|
||||
// const pageItems = await findAllByRole('button', { name: /1|2|3|下一页|上一页/i });
|
||||
// assertNotEquals(pageItems.length, 0, '未找到分页按钮');
|
||||
// });
|
||||
|
||||
// // 测试6: 操作按钮
|
||||
// await t.step('应显示操作按钮', async () => {
|
||||
// const editButtons = await findAllByText(/编辑/i);
|
||||
// assertNotEquals(editButtons.length, 0, '未找到编辑按钮');
|
||||
|
||||
// const deleteButtons = await findAllByText(/删除/i);
|
||||
// assertNotEquals(deleteButtons.length, 0, '未找到删除按钮');
|
||||
// });
|
||||
} finally {
|
||||
// 确保清理所有定时器
|
||||
cleanup();
|
||||
}
|
||||
},
|
||||
sanitizeOps: false, // 禁用操作清理检查
|
||||
sanitizeResources: false, // 禁用资源清理检查
|
||||
});
|
||||
@@ -24,22 +24,19 @@ import weekday from 'dayjs/plugin/weekday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import type {
|
||||
FileLibrary, FileCategory, KnowInfo
|
||||
KnowInfo
|
||||
} from '../share/types.ts';
|
||||
|
||||
import {
|
||||
AuditStatus,AuditStatusNameMap,
|
||||
OssType,
|
||||
} from '../share/types.ts';
|
||||
|
||||
import { getEnumOptions } from './utils.ts';
|
||||
|
||||
import {
|
||||
FileAPI,
|
||||
UserAPI,
|
||||
KnowInfoAPI,
|
||||
type KnowInfoListResponse
|
||||
} from './api.ts';
|
||||
} from './api/index.ts';
|
||||
|
||||
|
||||
// 配置 dayjs 插件
|
||||
@@ -49,7 +46,6 @@ dayjs.extend(localeData);
|
||||
// 设置 dayjs 语言
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
||||
// 知识库管理页面组件
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Layout, Menu, Button, Table, Space,
|
||||
Form, Input, Select, message, Modal,
|
||||
Card, Spin, Row, Col, Breadcrumb, Avatar,
|
||||
Dropdown, ConfigProvider, theme, Typography,
|
||||
Switch, Badge, Image, Upload, Divider, Descriptions,
|
||||
Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer,
|
||||
Tree
|
||||
Button, Space, Drawer,
|
||||
Select, message,
|
||||
Card, Spin, Typography,Descriptions,DatePicker,
|
||||
} from 'antd';
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
AppstoreOutlined,
|
||||
EnvironmentOutlined,
|
||||
SearchOutlined,
|
||||
ClockCircleOutlined,
|
||||
UserOutlined,
|
||||
GlobalOutlined
|
||||
@@ -28,7 +20,8 @@ import type {
|
||||
MarkerData, LoginLocation, LoginLocationDetail, User
|
||||
} from '../share/types.ts';
|
||||
|
||||
import { MapAPI,UserAPI } from './api.ts';
|
||||
import { UserAPI } from './api/index.ts';
|
||||
import { MapAPI } from './api/index.ts';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient, UseMutationResult } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
import { MessageAPI } from './api.ts';
|
||||
import { UserAPI } from './api.ts';
|
||||
import { MessageAPI , UserAPI } from './api/index.ts';
|
||||
import type { UserMessage } from '../share/types.ts';
|
||||
import { MessageStatusNameMap , MessageStatus} from '../share/types.ts';
|
||||
|
||||
export const MessagesPage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [form] = Form.useForm();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
page: 1,
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Layout, Menu, Button, Table, Space,
|
||||
Button,Space,
|
||||
Form, Input, Select, message, Modal,
|
||||
Card, Spin, Row, Col, Breadcrumb, Avatar,
|
||||
Dropdown, ConfigProvider, theme, Typography,
|
||||
Switch, Badge, Image, Upload, Divider, Descriptions,
|
||||
Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer, InputNumber,ColorPicker,
|
||||
Popover
|
||||
Card, Spin, Typography,
|
||||
Switch, Tabs, Alert, InputNumber
|
||||
} from 'antd';
|
||||
import {
|
||||
UploadOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
BgColorsOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
useQuery,
|
||||
useMutation,
|
||||
@@ -25,25 +19,20 @@ import weekday from 'dayjs/plugin/weekday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import type {
|
||||
FileLibrary, FileCategory, KnowInfo, SystemSetting, SystemSettingValue,
|
||||
ColorScheme
|
||||
SystemSetting, SystemSettingValue
|
||||
} from '../share/types.ts';
|
||||
import { ThemeMode } from '../share/types.ts';
|
||||
|
||||
import {
|
||||
SystemSettingGroup,
|
||||
SystemSettingKey,
|
||||
FontSize,
|
||||
CompactMode,
|
||||
AllowedFileType
|
||||
} from '../share/types.ts';
|
||||
|
||||
|
||||
import { getEnumOptions } from './utils.ts';
|
||||
|
||||
import {
|
||||
SystemAPI,
|
||||
} from './api.ts';
|
||||
SystemAPI
|
||||
} from './api/index.ts';
|
||||
|
||||
import { useTheme } from './hooks_sys.tsx';
|
||||
|
||||
@@ -241,44 +230,44 @@ export const SettingsPage = () => {
|
||||
items={Object.values(SystemSettingGroup).map(group => ({
|
||||
key: group,
|
||||
label: String(GROUP_TITLES[group]),
|
||||
children: (
|
||||
<div>
|
||||
<Alert
|
||||
children: (
|
||||
<div>
|
||||
<Alert
|
||||
message={GROUP_DESCRIPTIONS[group]}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{settingsData
|
||||
?.find(g => g.name === group)
|
||||
?.settings.map(setting => (
|
||||
<Form.Item
|
||||
<Form.Item
|
||||
key={setting.key}
|
||||
label={setting.description || setting.key}
|
||||
name={setting.key}
|
||||
rules={[{ required: true, message: `请输入${setting.description || setting.key}` }]}
|
||||
>
|
||||
{renderSettingInput(setting)}
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={updateSettingsMutation.isPending}
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={updateSettingsMutation.isPending}
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { JSDOM } from 'jsdom'
|
||||
import React from 'react'
|
||||
import {render, fireEvent, within, screen} from '@testing-library/react'
|
||||
import { ThemeSettingsPage } from "./pages_theme_settings.tsx"
|
||||
import { ThemeProvider } from "./hooks_sys.tsx"
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import {
|
||||
assertEquals,
|
||||
assertExists,
|
||||
assertNotEquals,
|
||||
assertRejects,
|
||||
} from "https://deno.land/std@0.217.0/assert/mod.ts";
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
|
||||
const dom = new JSDOM(`<body></body>`, {
|
||||
runScripts: "dangerously",
|
||||
pretendToBeVisual: true,
|
||||
url: "http://localhost"
|
||||
});
|
||||
|
||||
// 模拟浏览器环境
|
||||
globalThis.window = dom.window;
|
||||
globalThis.document = dom.window.document;
|
||||
|
||||
// 定义浏览器环境所需的类
|
||||
globalThis.Element = dom.window.Element;
|
||||
globalThis.HTMLElement = dom.window.HTMLElement;
|
||||
globalThis.ShadowRoot = dom.window.ShadowRoot;
|
||||
globalThis.SVGElement = dom.window.SVGElement;
|
||||
|
||||
// 模拟 getComputedStyle
|
||||
globalThis.getComputedStyle = (elt) => {
|
||||
const style = new dom.window.CSSStyleDeclaration();
|
||||
style.getPropertyValue = () => '';
|
||||
return style;
|
||||
};
|
||||
|
||||
// 模拟matchMedia函数
|
||||
globalThis.matchMedia = (query) => ({
|
||||
matches: query.includes('max-width'),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
});
|
||||
|
||||
// 模拟动画相关API
|
||||
globalThis.AnimationEvent = globalThis.AnimationEvent || dom.window.Event;
|
||||
globalThis.TransitionEvent = globalThis.TransitionEvent || dom.window.Event;
|
||||
|
||||
// 模拟requestAnimationFrame
|
||||
globalThis.requestAnimationFrame = globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 0));
|
||||
globalThis.cancelAnimationFrame = globalThis.cancelAnimationFrame || clearTimeout;
|
||||
|
||||
// 设置浏览器尺寸相关方法
|
||||
window.resizeTo = (width, height) => {
|
||||
window.innerWidth = width || window.innerWidth;
|
||||
window.innerHeight = height || window.innerHeight;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
};
|
||||
window.scrollTo = () => {};
|
||||
|
||||
const customScreen = within(document.body);
|
||||
|
||||
// 使用异步测试处理真实API调用
|
||||
Deno.test('主题设置页面测试', async (t) => {
|
||||
// 渲染组件
|
||||
const {findByRole, debug} = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<ThemeSettingsPage />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
// debug(await findByRole('radio', { name: /浅色模式/i }))
|
||||
|
||||
// 测试1: 渲染基本元素
|
||||
await t.step('应渲染主题设置标题', async () => {
|
||||
const title = await customScreen.findByText(/主题设置/i)
|
||||
assertExists(title, '未找到主题设置标题')
|
||||
})
|
||||
|
||||
// 测试2: 表单初始化状态
|
||||
await t.step('表单应正确初始化', async () => {
|
||||
// 检查主题模式选择
|
||||
const lightRadio = await customScreen.findByRole('radio', { name: /浅色模式/i })
|
||||
assertExists(lightRadio, '未找到浅色模式单选按钮')
|
||||
|
||||
// 检查主题模式标签
|
||||
const themeModeLabel = await customScreen.findByText(/主题模式/i)
|
||||
assertExists(themeModeLabel, '未找到主题模式标签')
|
||||
|
||||
// // 检查主题模式选择器 - Ant Design 使用 div 包裹 radio 而不是 radiogroup
|
||||
// const themeModeField = await customScreen.findByTestId('theme-mode-selector')
|
||||
// assertExists(themeModeField, '未找到主题模式选择器')
|
||||
})
|
||||
|
||||
// 测试3: 配色方案选择
|
||||
await t.step('应显示配色方案选项', async () => {
|
||||
// 查找预设配色方案标签
|
||||
const colorSchemeLabel = await customScreen.findByText('预设配色方案')
|
||||
assertExists(colorSchemeLabel, '未找到预设配色方案标签')
|
||||
|
||||
// 查找配色方案按钮
|
||||
const colorSchemeButtons = await customScreen.findAllByRole('button')
|
||||
assertNotEquals(colorSchemeButtons.length, 0, '未找到配色方案按钮')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,53 +1,30 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Layout, Menu, Button, Table, Space,
|
||||
Form, Input, Select, message, Modal,
|
||||
Card, Spin, Row, Col, Breadcrumb, Avatar,
|
||||
Dropdown, ConfigProvider, theme, Typography,
|
||||
Switch, Badge, Image, Upload, Divider, Descriptions,
|
||||
Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer, InputNumber,ColorPicker,
|
||||
Popover
|
||||
Button, Space,
|
||||
Form, message,
|
||||
Card, Spin, Typography,
|
||||
Switch,
|
||||
Popconfirm, Radio, InputNumber,ColorPicker,
|
||||
} from 'antd';
|
||||
import {
|
||||
UploadOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
BgColorsOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
useQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import type {
|
||||
FileLibrary, FileCategory, KnowInfo, SystemSetting, SystemSettingValue,
|
||||
ColorScheme
|
||||
} from '../share/types.ts';
|
||||
import { ThemeMode } from '../share/types.ts';
|
||||
|
||||
import {
|
||||
SystemSettingGroup,
|
||||
SystemSettingKey,
|
||||
FontSize,
|
||||
CompactMode,
|
||||
AllowedFileType
|
||||
} from '../share/types.ts';
|
||||
|
||||
|
||||
import { getEnumOptions } from './utils.ts';
|
||||
|
||||
import {
|
||||
SystemAPI,
|
||||
} from './api.ts';
|
||||
|
||||
import { useTheme } from './hooks_sys.tsx';
|
||||
|
||||
import { Uploader } from './components_uploader.tsx';
|
||||
|
||||
// 配置 dayjs 插件
|
||||
dayjs.extend(weekday);
|
||||
|
||||
270
client/admin/pages_users.tsx
Normal file
270
client/admin/pages_users.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button, Table, Space, Form, Input, Select,
|
||||
message, Modal, Card, Typography, Tag, Popconfirm
|
||||
} from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import { UserAPI } from './api/index.ts';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
// 用户管理页面
|
||||
export const UsersPage = () => {
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
search: ''
|
||||
});
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [editingUser, setEditingUser] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { data: usersData, isLoading, refetch } = useQuery({
|
||||
queryKey: ['users', searchParams],
|
||||
queryFn: async () => {
|
||||
return await UserAPI.getUsers(searchParams);
|
||||
}
|
||||
});
|
||||
|
||||
const users = usersData?.data || [];
|
||||
const pagination = {
|
||||
current: searchParams.page,
|
||||
pageSize: searchParams.limit,
|
||||
total: usersData?.pagination?.total || 0
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (values: any) => {
|
||||
setSearchParams(prev => ({
|
||||
...prev,
|
||||
search: values.search || '',
|
||||
page: 1
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理分页变化
|
||||
const handleTableChange = (newPagination: any) => {
|
||||
setSearchParams(prev => ({
|
||||
...prev,
|
||||
page: newPagination.current,
|
||||
limit: newPagination.pageSize
|
||||
}));
|
||||
};
|
||||
|
||||
// 打开创建用户模态框
|
||||
const showCreateModal = () => {
|
||||
setModalTitle('创建用户');
|
||||
setEditingUser(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 打开编辑用户模态框
|
||||
const showEditModal = (user: any) => {
|
||||
setModalTitle('编辑用户');
|
||||
setEditingUser(user);
|
||||
form.setFieldsValue(user);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理模态框确认
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (editingUser) {
|
||||
// 编辑用户
|
||||
await UserAPI.updateUser(editingUser.id, values);
|
||||
message.success('用户更新成功');
|
||||
} else {
|
||||
// 创建用户
|
||||
await UserAPI.createUser(values);
|
||||
message.success('用户创建成功');
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
refetch(); // 刷新用户列表
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error);
|
||||
message.error('操作失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除用户
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await UserAPI.deleteUser(id);
|
||||
message.success('用户删除成功');
|
||||
refetch(); // 刷新用户列表
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error);
|
||||
message.error('删除失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: string) => (
|
||||
<Tag color={role === 'admin' ? 'red' : 'blue'}>
|
||||
{role === 'admin' ? '管理员' : '普通用户'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="middle">
|
||||
<Button type="link" onClick={() => showEditModal(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除此用户吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={2}>用户管理</Title>
|
||||
<Card>
|
||||
<Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
|
||||
<Form.Item name="search" label="搜索">
|
||||
<Input placeholder="用户名/昵称/邮箱" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
搜索
|
||||
</Button>
|
||||
<Button type="primary" onClick={showCreateModal}>
|
||||
创建用户
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 创建/编辑用户模态框 */}
|
||||
<Modal
|
||||
title={modalTitle}
|
||||
open={modalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
}}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="nickname"
|
||||
label="昵称"
|
||||
rules={[{ required: true, message: '请输入昵称' }]}
|
||||
>
|
||||
<Input placeholder="请输入昵称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
|
||||
{!editingUser && (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' }
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="角色"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select placeholder="请选择角色">
|
||||
<Select.Option value="user">普通用户</Select.Option>
|
||||
<Select.Option value="admin">管理员</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -55,10 +55,14 @@ import {
|
||||
} from './hooks_sys.tsx';
|
||||
|
||||
import {
|
||||
DashboardPage,
|
||||
UsersPage,
|
||||
DashboardPage
|
||||
} from './pages_dashboard.tsx';
|
||||
import {
|
||||
UsersPage
|
||||
} from './pages_users.tsx';
|
||||
import {
|
||||
FileLibraryPage
|
||||
} from './pages_sys.tsx';
|
||||
} from './pages_file_library.tsx';
|
||||
import { KnowInfoPage } from './pages_know_info.tsx';
|
||||
import { MessagesPage } from './pages_messages.tsx';
|
||||
import {SettingsPage } from './pages_settings.tsx';
|
||||
|
||||
Reference in New Issue
Block a user