473 lines
15 KiB
TypeScript
473 lines
15 KiB
TypeScript
/**
|
||
* 入口模块管理 API 客户端
|
||
* 提供入口模块的增删改查功能
|
||
* API 文档: auth_doc/ENTRY_MODULE_API.md
|
||
*/
|
||
|
||
import { get, post, put, del } from "../axios-client";
|
||
|
||
/**
|
||
* 地区配置接口
|
||
*/
|
||
export interface AreaConfig {
|
||
area: string; // 地区名称(如:梅州、云浮、揭阳、潮州)
|
||
enabled: boolean; // 是否启用(默认 true)
|
||
sort_order: number; // 排序号(默认 0)
|
||
}
|
||
|
||
/**
|
||
* 入口模块数据接口
|
||
*/
|
||
export interface EntryModule {
|
||
id?: number;
|
||
name: string;
|
||
description?: string | null;
|
||
path?: string | null; // logo图片路径
|
||
route_path?: string | null; // 前端跳转路径
|
||
areas?: AreaConfig[] | null; // 地区配置列表
|
||
created_at?: string;
|
||
updated_at?: string;
|
||
}
|
||
|
||
/**
|
||
* 入口模块搜索参数
|
||
*/
|
||
export interface EntryModuleSearchParams {
|
||
name?: string;
|
||
area?: string;
|
||
page?: number;
|
||
page_size?: number; // 注意:API使用page_size而不是pageSize
|
||
}
|
||
|
||
/**
|
||
* 入口模块列表响应
|
||
*/
|
||
export interface EntryModulesResponse {
|
||
modules: EntryModule[];
|
||
total: number;
|
||
}
|
||
|
||
/**
|
||
* API统一响应格式
|
||
* 注意:后端返回的字段是 msg 而不是 message,code 为 0 表示成功
|
||
*/
|
||
interface ApiResponse<T> {
|
||
code: number; // 0 表示成功
|
||
msg: string; // 消息(不是 message)
|
||
data: T;
|
||
}
|
||
|
||
/**
|
||
* 列表数据响应格式
|
||
*/
|
||
interface ListResponse<T> {
|
||
total: number;
|
||
page: number;
|
||
page_size: number;
|
||
items: T[];
|
||
}
|
||
|
||
/**
|
||
* 获取入口模块列表
|
||
* @param searchParams 搜索参数
|
||
* @returns 入口模块列表和总数
|
||
*
|
||
* 注意:
|
||
* 1. 不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
* 2. 后端返回的 data.items 会被转换为 data.modules(语义化重命名)
|
||
*/
|
||
export async function getEntryModules(
|
||
searchParams: EntryModuleSearchParams = {}
|
||
): Promise<{ data?: EntryModulesResponse; error?: string }> {
|
||
try {
|
||
const { name, area, page = 1, page_size = 10 } = searchParams;
|
||
|
||
// 构建查询参数
|
||
const queryParams: Record<string, any> = {
|
||
page,
|
||
page_size
|
||
};
|
||
|
||
if (name) {
|
||
queryParams.name = name;
|
||
}
|
||
|
||
if (area) {
|
||
queryParams.area = area;
|
||
}
|
||
|
||
// 调用 API
|
||
const response = await get<ApiResponse<ListResponse<EntryModule>>>(
|
||
'/api/v3/entry-modules',
|
||
queryParams
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [getEntryModules] API错误:', response.error);
|
||
return { error: response.error };
|
||
}
|
||
|
||
// 🔑 解析响应数据
|
||
// axios-client 返回: { data: {后端JSON}, status: 200 }
|
||
// 后端返回: { code: 0, msg: "成功", data: { total, items } }
|
||
const backendResponse = response.data;
|
||
|
||
if (!backendResponse) {
|
||
console.error('❌ [getEntryModules] 响应为空');
|
||
return { error: '响应为空' };
|
||
}
|
||
|
||
// 检查后端返回的 code(0 表示成功)
|
||
if (backendResponse.code !== 0) {
|
||
console.error('❌ [getEntryModules] 后端返回错误:', backendResponse.msg);
|
||
return { error: backendResponse.msg || '请求失败' };
|
||
}
|
||
|
||
const apiData = backendResponse.data;
|
||
if (!apiData) {
|
||
console.error('❌ [getEntryModules] data 字段为空');
|
||
return { error: '数据为空' };
|
||
}
|
||
|
||
return {
|
||
data: {
|
||
modules: apiData.items || [],
|
||
total: apiData.total || 0
|
||
}
|
||
};
|
||
} catch (error) {
|
||
console.error("❌ [getEntryModules] 获取入口模块列表失败:", error);
|
||
return { error: error instanceof Error ? error.message : "获取入口模块列表失败" };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据ID获取入口模块
|
||
* @param id 入口模块ID
|
||
* @returns 入口模块数据
|
||
*
|
||
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
*/
|
||
export async function getEntryModuleById(
|
||
id: number
|
||
): Promise<{ data?: EntryModule; error?: string }> {
|
||
try {
|
||
// 调用 API
|
||
const response = await get<ApiResponse<EntryModule>>(
|
||
`/api/v3/entry-modules/${id}`
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [getEntryModuleById] API错误:', response.error);
|
||
return { error: response.error };
|
||
}
|
||
|
||
const backendResponse = response.data;
|
||
if (!backendResponse || backendResponse.code !== 0) {
|
||
console.error('❌ [getEntryModuleById] 后端返回错误:', backendResponse?.msg);
|
||
return { error: backendResponse?.msg || '获取失败' };
|
||
}
|
||
|
||
const module = backendResponse.data;
|
||
if (!module) {
|
||
console.error('❌ [getEntryModuleById] 入口模块不存在');
|
||
return { error: "入口模块不存在" };
|
||
}
|
||
|
||
return { data: module };
|
||
} catch (error) {
|
||
console.error("❌ [getEntryModuleById] 获取入口模块失败:", error);
|
||
return { error: error instanceof Error ? error.message : "获取入口模块失败" };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建入口模块
|
||
* @param module 入口模块数据
|
||
* @returns 创建的入口模块
|
||
*
|
||
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
*/
|
||
export async function createEntryModule(
|
||
module: {
|
||
name: string;
|
||
description?: string;
|
||
path?: string | null;
|
||
areas?: string[] | AreaConfig[]; // 兼容两种格式
|
||
}
|
||
): Promise<{ data?: EntryModule; error?: string }> {
|
||
try {
|
||
// 转换 areas 格式(如果是字符串数组,转换为 AreaConfig 数组)
|
||
let areasConfig: AreaConfig[] | undefined;
|
||
if (module.areas && Array.isArray(module.areas)) {
|
||
if (typeof module.areas[0] === 'string') {
|
||
// 字符串数组转换为 AreaConfig 数组
|
||
areasConfig = (module.areas as string[]).map((area, index) => ({
|
||
area,
|
||
enabled: true,
|
||
sort_order: index + 1
|
||
}));
|
||
} else {
|
||
// 已经是 AreaConfig 数组
|
||
areasConfig = module.areas as AreaConfig[];
|
||
}
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestBody = {
|
||
name: module.name,
|
||
description: module.description || undefined,
|
||
path: module.path || undefined,
|
||
areas: areasConfig
|
||
};
|
||
|
||
// 调用 API
|
||
const response = await post<ApiResponse<EntryModule>>(
|
||
'/api/v3/entry-modules',
|
||
requestBody
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [createEntryModule] API错误:', response.error);
|
||
return { error: response.error };
|
||
}
|
||
|
||
const backendResponse = response.data;
|
||
if (!backendResponse || backendResponse.code !== 0) {
|
||
console.error('❌ [createEntryModule] 后端返回错误:', backendResponse?.msg);
|
||
return { error: backendResponse?.msg || '创建失败' };
|
||
}
|
||
|
||
const createdModule = backendResponse.data;
|
||
if (!createdModule) {
|
||
console.error('❌ [createEntryModule] 响应数据格式错误');
|
||
return { error: '创建失败:响应数据格式错误' };
|
||
}
|
||
|
||
return { data: createdModule };
|
||
} catch (error) {
|
||
console.error("❌ [createEntryModule] 创建入口模块失败:", error);
|
||
return { error: error instanceof Error ? error.message : "创建入口模块失败" };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新入口模块
|
||
* @param id 入口模块ID
|
||
* @param module 更新的入口模块数据
|
||
* @returns 更新的入口模块
|
||
*
|
||
* ⚠️ 重要:areas 字段是完全替换,不是增量更新!
|
||
* 如果只想修改某个地区的状态,需要:
|
||
* 1. 先调用 getEntryModuleById 获取完整的 areas 数组
|
||
* 2. 在前端修改 areas 数组中的目标项
|
||
* 3. 将完整的 areas 数组传给此接口
|
||
*
|
||
* 示例:
|
||
* ```typescript
|
||
* // 错误做法:只传要修改的地区(会覆盖其他地区配置)
|
||
* await updateEntryModule(1, {
|
||
* areas: [{ area: "梅州", enabled: false, sort_order: 1 }]
|
||
* });
|
||
*
|
||
* // 正确做法:先获取完整数据,修改后整体更新
|
||
* const { data: module } = await getEntryModuleById(1);
|
||
* const updatedAreas = module!.areas!.map(config =>
|
||
* config.area === "梅州" ? { ...config, enabled: false } : config
|
||
* );
|
||
* await updateEntryModule(1, { areas: updatedAreas });
|
||
* ```
|
||
*
|
||
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
*/
|
||
export async function updateEntryModule(
|
||
id: number,
|
||
module: {
|
||
name?: string;
|
||
description?: string;
|
||
path?: string | null;
|
||
areas?: string[] | AreaConfig[]; // 兼容两种格式
|
||
}
|
||
): Promise<{ data?: EntryModule; error?: string }> {
|
||
try {
|
||
// 转换 areas 格式(如果是字符串数组,转换为 AreaConfig 数组)
|
||
let areasConfig: AreaConfig[] | undefined;
|
||
if (module.areas && Array.isArray(module.areas)) {
|
||
if (typeof module.areas[0] === 'string') {
|
||
// 字符串数组转换为 AreaConfig 数组
|
||
areasConfig = (module.areas as string[]).map((area, index) => ({
|
||
area,
|
||
enabled: true,
|
||
sort_order: index + 1
|
||
}));
|
||
} else {
|
||
// 已经是 AreaConfig 数组
|
||
areasConfig = module.areas as AreaConfig[];
|
||
}
|
||
}
|
||
|
||
// 构建请求体(只包含需要更新的字段)
|
||
const requestBody: any = {};
|
||
if (module.name !== undefined) requestBody.name = module.name;
|
||
if (module.description !== undefined) requestBody.description = module.description;
|
||
if (module.path !== undefined) requestBody.path = module.path;
|
||
if (areasConfig !== undefined) requestBody.areas = areasConfig;
|
||
|
||
// 调用 API
|
||
const response = await put<ApiResponse<EntryModule>>(
|
||
`/api/v3/entry-modules/${id}`,
|
||
requestBody
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [updateEntryModule] API错误:', response.error);
|
||
return { error: response.error };
|
||
}
|
||
|
||
const backendResponse = response.data;
|
||
if (!backendResponse || backendResponse.code !== 0) {
|
||
console.error('❌ [updateEntryModule] 后端返回错误:', backendResponse?.msg);
|
||
return { error: backendResponse?.msg || '更新失败' };
|
||
}
|
||
|
||
const updatedModule = backendResponse.data;
|
||
if (!updatedModule) {
|
||
console.error('❌ [updateEntryModule] 响应数据格式错误');
|
||
return { error: '更新失败:响应数据格式错误' };
|
||
}
|
||
|
||
return { data: updatedModule };
|
||
} catch (error) {
|
||
console.error("❌ [updateEntryModule] 更新入口模块失败:", error);
|
||
return { error: error instanceof Error ? error.message : "更新入口模块失败" };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除入口模块
|
||
* @param id 入口模块ID
|
||
* @returns 是否成功
|
||
*
|
||
* ⚠️ 警告:
|
||
* 1. 删除操作是物理删除,数据库记录直接删除,不可恢复
|
||
* 2. MinIO中的图标文件不会自动删除,需要手动清理或通过定时任务清理孤立文件
|
||
* 3. 如果其他表有外键引用,可能会触发级联删除或报错(取决于外键约束设置)
|
||
*
|
||
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
*/
|
||
export async function deleteEntryModule(
|
||
id: number
|
||
): Promise<{ success: boolean; error?: string }> {
|
||
try {
|
||
// 调用 API
|
||
const response = await del<ApiResponse<{ message: string }>>(
|
||
`/api/v3/entry-modules/${id}`
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [deleteEntryModule] API错误:', response.error);
|
||
return { success: false, error: response.error };
|
||
}
|
||
|
||
const backendResponse = response.data;
|
||
if (!backendResponse || backendResponse.code !== 0) {
|
||
console.error('❌ [deleteEntryModule] 删除失败:', backendResponse?.msg);
|
||
return { success: false, error: backendResponse?.msg || '删除失败' };
|
||
}
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error("❌ [deleteEntryModule] 删除入口模块失败:", error);
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : "删除入口模块失败"
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 图片上传响应数据
|
||
*/
|
||
export interface ImageUploadResponse {
|
||
module_id: number;
|
||
path: string;
|
||
url: string;
|
||
message: string;
|
||
}
|
||
|
||
/**
|
||
* 上传入口模块图标
|
||
* @param id 入口模块ID
|
||
* @param file 图片文件
|
||
* @returns 上传结果(包含图片路径和访问URL)
|
||
*
|
||
* ⚠️ 重要注意事项:
|
||
* 1. FormData字段名**必须**是 "file",不能是 "image" 或 "picture"
|
||
* 2. **不要**手动设置 Content-Type,axios会自动处理multipart/form-data的boundary
|
||
* 3. 上传成功后,后端会自动更新数据库中的 path 字段,前端无需手动调用更新接口
|
||
* 4. 支持的文件格式:.jpg, .jpeg, .png, .gif, .webp, .svg
|
||
* 5. 每次上传会覆盖该模块的旧图标(相同格式直接覆盖,不同格式会删除旧文件)
|
||
* 6. 不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
*
|
||
* 常见错误:
|
||
* - "Field required - body.file" → 检查FormData字段名是否为 "file"
|
||
* - 上传失败但无明确错误 → 检查是否手动设置了Content-Type(应该让axios自动处理)
|
||
* - 图片无法访问(401) → 旧版本问题,现已修复,MinIO路径已加入JWT白名单
|
||
*
|
||
* 使用示例:
|
||
* ```typescript
|
||
* const formData = new FormData();
|
||
* formData.append('file', imageFile); // ✅ 字段名必须是 "file"
|
||
* const result = await uploadEntryModuleImage(moduleId, imageFile);
|
||
* console.log('图片URL:', result.data?.url);
|
||
* ```
|
||
*/
|
||
export async function uploadEntryModuleImage(
|
||
id: number,
|
||
file: File
|
||
): Promise<{ data?: ImageUploadResponse; error?: string }> {
|
||
try {
|
||
// 构建 FormData
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
console.log('📤 [uploadEntryModuleImage] 开始上传:', {
|
||
moduleId: id,
|
||
fileName: file.name,
|
||
fileSize: file.size,
|
||
fileType: file.type
|
||
});
|
||
|
||
// 调用 API
|
||
// 重要:不要手动设置 Content-Type,让 axios 自动处理
|
||
// axios 会自动将 FormData 的 Content-Type 设置为 multipart/form-data 并添加 boundary
|
||
const response = await post<ApiResponse<ImageUploadResponse>>(
|
||
`/api/v3/entry-modules/${id}/image`,
|
||
formData
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [uploadEntryModuleImage] API错误:', response.error);
|
||
return { error: response.error };
|
||
}
|
||
|
||
const backendResponse = response.data;
|
||
if (!backendResponse || backendResponse.code !== 0) {
|
||
console.error('❌ [uploadEntryModuleImage] 上传失败:', backendResponse?.msg);
|
||
return { error: backendResponse?.msg || '上传失败' };
|
||
}
|
||
|
||
const uploadResult = backendResponse.data;
|
||
if (!uploadResult) {
|
||
console.error('❌ [uploadEntryModuleImage] 响应数据格式错误');
|
||
return { error: '上传失败:响应数据格式错误' };
|
||
}
|
||
|
||
console.log('✅ [uploadEntryModuleImage] 上传成功:', uploadResult);
|
||
return { data: uploadResult };
|
||
} catch (error) {
|
||
console.error("❌ [uploadEntryModuleImage] 上传入口模块图标失败:", error);
|
||
return { error: error instanceof Error ? error.message : "上传入口模块图标失败" };
|
||
}
|
||
}
|