fix: 1. 全局axios添加formData文件上传的检测,删除Content-Type让axios自动检测。

2. 完善入口模块管理的接口的对接。
3. 完善角色权限管理的接口对接和测试。
4. 完善主页的入口模块的图标的显示和图片的显示。
This commit is contained in:
2025-11-29 19:37:29 +08:00
parent 5600de413f
commit fb67f138dc
11 changed files with 290 additions and 164 deletions
+6
View File
@@ -80,6 +80,12 @@ function isInErrorTolerantWhitelist(url?: string): boolean {
*/
axiosInstance.interceptors.request.use(
(config) => {
// ⭐ 检测 FormData,删除默认的 Content-Type,让 axios 自动处理
if (config.data instanceof FormData) {
console.log('📦 [Request Interceptor] 检测到FormData,删除Content-Type让axios自动处理');
delete config.headers['Content-Type'];
}
// 检查是否在白名单中
if (isInAuthWhitelist(config.url)) {
console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url);
+116 -2
View File
@@ -71,7 +71,9 @@ interface ListResponse<T> {
* @param searchParams 搜索参数
* @returns 入口模块列表和总数
*
* 注意:不需要传递 JWTaxios-client 会自动从 localStorage 读取并添加
* 注意:
* 1. 不需要传递 JWTaxios-client 会自动从 localStorage 读取并添加
* 2. 后端返回的 data.items 会被转换为 data.modules(语义化重命名)
*/
export async function getEntryModules(
searchParams: EntryModuleSearchParams = {}
@@ -254,6 +256,27 @@ export async function createEntryModule(
* @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 });
* ```
*
* 注意:不需要传递 JWTaxios-client 会自动从 localStorage 读取并添加
*/
export async function updateEntryModule(
@@ -324,6 +347,11 @@ export async function updateEntryModule(
* @param id 入口模块ID
* @returns 是否成功
*
* ⚠️ 警告:
* 1. 删除操作是物理删除,数据库记录直接删除,不可恢复
* 2. MinIO中的图标文件不会自动删除,需要手动清理或通过定时任务清理孤立文件
* 3. 如果其他表有外键引用,可能会触发级联删除或报错(取决于外键约束设置)
*
* 注意:不需要传递 JWTaxios-client 会自动从 localStorage 读取并添加
*/
export async function deleteEntryModule(
@@ -331,7 +359,7 @@ export async function deleteEntryModule(
): Promise<{ success: boolean; error?: string }> {
try {
// 调用 API
const response = await del<ApiResponse<{ id: number; deleted: boolean }>>(
const response = await del<ApiResponse<{ message: string }>>(
`/api/v3/entry-modules/${id}`
);
@@ -355,3 +383,89 @@ export async function deleteEntryModule(
};
}
}
/**
* 图片上传响应数据
*/
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-Typeaxios会自动处理multipart/form-data的boundary
* 3. 上传成功后,后端会自动更新数据库中的 path 字段,前端无需手动调用更新接口
* 4. 支持的文件格式:.jpg, .jpeg, .png, .gif, .webp, .svg
* 5. 每次上传会覆盖该模块的旧图标(相同格式直接覆盖,不同格式会删除旧文件)
* 6. 不需要传递 JWTaxios-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 : "上传入口模块图标失败" };
}
}
+29 -7
View File
@@ -450,7 +450,9 @@ export async function saveRoleApiPermissions(
data_scope: 'ALL'
}));
const response = await post<any>(`/api/v3/rbac/roles/${roleId}/permissions`, {
// v3.4: 使用文档规范的API路径(查询参数方式)
const response = await post<any>('/api/v3/rbac/role-permissions', {
role_id: roleId,
permissions,
replace: true // 替换模式:先删除现有权限,再插入新权限
});
@@ -621,8 +623,10 @@ export async function getAllUsers(params?: {
if (params?.page_size) queryParams.page_size = params.page_size;
if (params?.username) queryParams.search = params.username;
// 使用 axios-client 的 get 函数,会自动添加 baseURL 和 Authorization
const response = await get<any>('/admin/users/users', queryParams);
// v3.3: 使用标准 RBAC API,后端会自动根据用户角色进行地区过滤
// 省级管理员: 返回所有地区用户
// 市级管理员: 只返回同地区用户
const response = await get<any>('/api/v2/users', queryParams);
if (response.error) {
throw new Error(response.error);
@@ -643,6 +647,8 @@ export async function getAllUsers(params?: {
}
}
console.log('获取的用户列表', users )
const userList = users.map(user => ({
id: user.user_id || user.id, // 优先使用 user_id,兼容不同的后端响应格式
@@ -685,6 +691,15 @@ export async function assignUserRoles(
throw new Error(response.error);
}
// v3.4: 检查后端返回的code200表示成功)
if (response.data && response.data.code && response.data.code !== 200) {
// 后端返回错误,如权限不足(403)等
return {
success: false,
message: response.data.message || '分配失败'
};
}
// 后端响应格式: { code: 200, message: "角色分配成功", data: { user_id, roles: [...] } }
let message = '用户角色分配成功';
if (response.data && response.data.message) {
@@ -995,7 +1010,10 @@ export async function getRolePermissions(roleId: number): Promise<RolePermission
try {
const { get } = await import('~/api/axios-client');
const response = await get<any>(`/api/v3/rbac/roles/${roleId}/permissions`);
// v3.4: 使用文档规范的API路径(查询参数方式)
const response = await get<any>('/api/v3/rbac/role-permissions', {
role_id: roleId
});
if (response.error) {
throw new Error(response.error);
@@ -1039,7 +1057,9 @@ export async function assignPermissionsToRole(
replace = false
): Promise<{ success: boolean; message: string }> {
try {
const response = await post<any>(`${RBAC_API_BASE}/roles/${roleId}/permissions`, {
// v3.4: 使用文档规范的API路径(查询参数方式)
const response = await post<any>(`${RBAC_API_BASE}/role-permissions`, {
role_id: roleId,
permissions: permissions.map(p => ({
permission_id: p.permission_id,
grant_type: p.grant_type || 'GRANT',
@@ -1072,8 +1092,9 @@ export async function updateRolePermission(
config: Partial<RolePermissionConfig>
): Promise<{ success: boolean; message: string }> {
try {
// v3.4: 使用文档规范的API路径(查询参数方式)
const response = await put<any>(
`${RBAC_API_BASE}/roles/${roleId}/permissions/${permissionId}`,
`${RBAC_API_BASE}/role-permissions?role_id=${roleId}&permission_id=${permissionId}`,
config
);
handleApiResponse<any>(response);
@@ -1098,7 +1119,8 @@ export async function revokeRolePermission(
permissionId: number
): Promise<{ success: boolean; message: string }> {
try {
const response = await del<any>(`${RBAC_API_BASE}/roles/${roleId}/permissions/${permissionId}`);
// v3.4: 使用文档规范的API路径(查询参数方式)
const response = await del<any>(`${RBAC_API_BASE}/role-permissions?role_id=${roleId}&permission_id=${permissionId}`);
handleApiResponse<any>(response);
return { success: true, message: '权限移除成功' };