/** * 入口模块管理 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 { code: number; // 0 表示成功 msg: string; // 消息(不是 message) data: T; } /** * 列表数据响应格式 */ interface ListResponse { 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 = { page, page_size }; if (name) { queryParams.name = name; } if (area) { queryParams.area = area; } // 调用 API const response = await get>>( '/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>( `/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>( '/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>( `/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>( `/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>( `/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 : "上传入口模块图标失败" }; } }