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
+13 -9
View File
@@ -5,6 +5,7 @@ import styles from "~/styles/pages/home.css?url";
import dayjs from 'dayjs';
import { getUserSession, logout } from "~/api/login/auth.server";
import { toastService } from '~/components/ui';
import { DOCUMENT_URL } from '~/config/api-config';
export const links = () => [
{ rel: "stylesheet", href: styles }
@@ -227,14 +228,17 @@ export default function Index() {
// 获取模块图标(根据模块 path 或 id)
const getModuleIcon = (module: typeof loaderData.entryModules[0]) => {
// 根据 path 判断图标
if (module.path?.includes('ht')) {
return '/images/icon_hetong.png';
} else if (module.path?.includes('aj')) {
return '/images/icon_anjuan.png';
} else if (module.path?.includes('nw')) {
return '/images/icon_assistant.png';
}
// if (module.path?.includes('ht')) {
// return '/images/icon_hetong.png';
// } else if (module.path?.includes('aj')) {
// return '/images/icon_anjuan.png';
// } else if (module.path?.includes('nw')) {
// return '/images/icon_assistant.png';
// }
// 默认图标
if (module.path){
return `${DOCUMENT_URL}${module.path}`
}
return '/images/icon_assistant.png';
};
@@ -388,7 +392,7 @@ export default function Index() {
aria-label="交叉评查"
>
<img
src="/images/icon_cross_checking.png"
src="/images/icon_assistant.png"
alt="交叉评查"
className="w-12 h-12 mx-1"
onError={(e) => {
@@ -418,7 +422,7 @@ export default function Index() {
aria-label={module.name}
>
<img
src={getModuleIcon(module)}
src={isLLMModule ? '/images/icon_assistant.png' : getModuleIcon(module)}
alt={module.name}
className="w-12 h-12 mx-1"
/>
+2 -2
View File
@@ -299,7 +299,7 @@ export default function DocumentTypesList() {
{
title: "文档类型名称",
key: "name",
width: "180px",
width: "200px",
render: (_: unknown, record: DocumentTypeUI) => (
<div className="flex items-center">
<i className="ri-file-text-line text-primary mr-2"></i>
@@ -454,7 +454,7 @@ export default function DocumentTypesList() {
placeholder="请输入文档类型名称"
value={name}
onSearch={handleNameSearch}
className="flex-1 min-w-[200px]"
className="min-w-[400px]"
instantSearch={true}
/>
+38 -19
View File
@@ -110,6 +110,9 @@ export default function EntryModulesList() {
const [isDeleting, setIsDeleting] = useState(false);
const revalidator = useRevalidator();
// 记录加载失败的图片ID
const [failedImages, setFailedImages] = useState<Set<number>>(new Set());
// 获取加载器数据
const loaderData = useLoaderData<LoaderData>();
const { modules, total, error } = loaderData;
@@ -259,26 +262,41 @@ export default function EntryModulesList() {
if (!record.path) {
return <span className="text-gray-400"></span>;
}
const logoUrl = `${DOCUMENT_URL}${record.path}`;
const hasFailed = failedImages.has(record.id!);
return (
<div className="flex items-center">
<img
src={logoUrl}
alt={record.name}
className="h-8 w-8 object-contain rounded"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).parentElement!.innerHTML = '<span class="text-red-500">加载失败</span>';
}}
/>
<a
href={logoUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-blue-600 hover:underline text-sm"
>
</a>
{!hasFailed ? (
<>
<a
href={logoUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-blue-600 hover:underline text-sm"
>
<div className="h-8 w-8 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
<img
src={logoUrl}
alt={record.name}
className="h-full w-full object-contain"
onError={() => {
// 使用 React 状态管理加载失败的图片
setFailedImages(prev => new Set(prev).add(record.id!));
}}
/>
</div>
</a>
</>
) : (
<div className="flex items-center">
<div className="h-8 w-8 bg-red-50 border border-red-200 rounded flex items-center justify-center">
<i className="ri-image-line text-red-400 text-lg"></i>
</div>
<span className="ml-2 text-red-500 text-sm"></span>
</div>
)}
</div>
);
}
@@ -396,10 +414,11 @@ export default function EntryModulesList() {
{/* 分页 */}
{total > 0 && (
<Pagination
current={currentPage}
// pageSizeOptions={[10,20]}
currentPage={currentPage}
pageSize={pageSize}
total={total}
onPageChange={handlePageChange}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
)}
+24 -87
View File
@@ -1,19 +1,18 @@
import { useState, useEffect, useRef } from "react";
import { useNavigate, useSearchParams, useLoaderData } from "@remix-run/react";
import { ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
import { useNavigate, useSearchParams, useLoaderData, ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { toastService } from "~/components/ui/Toast";
import { Modal } from "~/components/ui/Modal";
import { usePermission } from "~/hooks/usePermission";
import {
getEntryModuleById,
createEntryModule,
updateEntryModule,
uploadEntryModuleImage,
type EntryModule,
type AreaConfig
} from "~/api/entry-modules/entry-modules";
import { API_BASE_URL, DOCUMENT_URL } from "~/config/api-config";
import { DOCUMENT_URL } from "~/config/api-config";
import entryModulesStyles from "~/styles/pages/entry-modules.css?url";
// 引入CSS样式
@@ -110,7 +109,6 @@ export default function EntryModuleNew() {
module?.path ? `${DOCUMENT_URL}${module.path}` : null
);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -185,48 +183,6 @@ export default function EntryModuleNew() {
return true;
};
// 上传logo图片
const uploadLogo = async (): Promise<string | null> => {
if (!logoFile) return module?.path || null;
try {
const formData = new FormData();
formData.append('file', logoFile);
formData.append('folder', 'entryModule');
// ✅ 不需要手动添加 Authorization 头
// fetch 可以自动使用浏览器的认证信息,或者我们也可以使用 axios
const token = localStorage.getItem('access_token');
const response = await fetch(`${API_BASE_URL}/admin/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (!response.ok) {
throw new Error('图片上传失败');
}
const result = await response.json();
console.log('图片上传结果:', result);
// 根据后端返回的数据结构提取路径
if (result.data?.path) {
return result.data.path;
} else if (result.path) {
return result.path;
} else {
throw new Error('未获取到图片路径');
}
} catch (error) {
console.error('上传logo失败:', error);
throw error;
}
};
// 处理表单提交
const handleSubmit = async () => {
// ✅ Runtime permission check
@@ -245,27 +201,26 @@ export default function EntryModuleNew() {
setIsSubmitting(true);
try {
// 上传logo
let logoPath = module?.path || null;
if (logoFile) {
logoPath = await uploadLogo();
}
// 🔑 准备提交数据
// 🔑 步骤1: 先创建或更新模块(不包含图片上传)
// areas 字段会在 API 层自动转换为 AreaConfig[] 格式
const moduleData = {
name: name.trim(),
description: description.trim() || undefined,
path: logoPath,
// 创建时 path 为 null,编辑时保持原 path(图片上传接口会自动更新)
path: isEditMode ? module?.path : null,
areas: selectedAreas // 字符串数组,API会自动转换
};
// ✅ 不需要传递 JWTaxios-client 会自动处理
let result;
let moduleId: number;
if (isEditMode) {
result = await updateEntryModule(parseInt(id!), moduleData);
moduleId = parseInt(id!);
} else {
result = await createEntryModule(moduleData);
moduleId = result.data!.id!;
}
if (result.error) {
@@ -273,6 +228,19 @@ export default function EntryModuleNew() {
return;
}
// 🔑 步骤2: 如果有新的logo文件,调用专用的图片上传接口
// 后端会自动更新数据库中的 path 字段,无需再手动调用 update
if (logoFile) {
const uploadResult = await uploadEntryModuleImage(moduleId, logoFile);
if (uploadResult.error) {
toastService.error(`模块${isEditMode ? '更新' : '创建'}成功,但图片上传失败: ${uploadResult.error}`);
return;
}
console.log('✅ 图片上传成功,访问URL:', uploadResult.data?.url);
}
toastService.success(isEditMode ? '更新成功!' : '创建成功!');
setTimeout(() => {
navigate('/entry-modules');
@@ -287,14 +255,10 @@ export default function EntryModuleNew() {
// 处理取消
const handleCancel = () => {
setShowConfirmModal(true);
};
// 确认取消
const confirmCancel = () => {
navigate('/entry-modules');
};
return (
<div className="entry-modules-new-page">
<Card>
@@ -429,33 +393,6 @@ export default function EntryModuleNew() {
)}
</div>
</Card>
{/* 取消确认模态框 */}
<Modal
isOpen={showConfirmModal}
onClose={() => setShowConfirmModal(false)}
title="确认取消"
size="small"
footer={
<div className="flex justify-end space-x-3">
<Button
type="default"
onClick={() => setShowConfirmModal(false)}
>
</Button>
<Button
type="primary"
danger
onClick={confirmCancel}
>
</Button>
</div>
}
>
<p className="text-gray-700"></p>
</Modal>
</div>
);
}
+7 -5
View File
@@ -34,11 +34,13 @@ export function links() {
}
// 面包屑导航
// export const handle = {
// breadcrumb: () => {
// return '上传文件'
// }
// }
export const handle = {
breadcrumb: "文档列表",
previousRoute: {
title: "文档列表",
to: "/documents/list"
}
};
export const meta: MetaFunction = () => {
return [
+41 -24
View File
@@ -50,7 +50,7 @@ export const meta = () => {
function getDataScopeLabel(scope: string): string {
const map: Record<string, string> = {
'ALL': '全部数据',
'DEPT': '部门数据',
'DEPT': '地市级数据',
'SELF': '仅本人数据'
};
return map[scope] || scope;
@@ -314,13 +314,13 @@ function CreateRoleModal({ isOpen, onClose, onSuccess }: CreateRoleModalProps) {
disabled={loading}
>
<option value="SELF"></option>
<option value="DEPT"></option>
<option value="DEPT"></option>
<option value="ALL"></option>
</select>
<span className="form-hint">访</span>
</div>
<div className="form-group">
{/* <div className="form-group">
<label>优先级</label>
<input
type="number"
@@ -333,7 +333,7 @@ function CreateRoleModal({ isOpen, onClose, onSuccess }: CreateRoleModalProps) {
disabled={loading}
/>
<span className="form-hint">数字越小优先级越高,范围 1-100</span>
</div>
</div> */}
</form>
</Modal>
);
@@ -495,7 +495,7 @@ function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps)
disabled={loading || (role.is_system_role && role.role_key === 'provincial_admin')}
>
<option value="SELF"></option>
<option value="DEPT"></option>
<option value="DEPT"></option>
<option value="ALL"></option>
</select>
{role.is_system_role && role.role_key === 'provincial_admin' && (
@@ -503,7 +503,7 @@ function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps)
)}
</div>
<div className="form-group">
{/* <div className="form-group">
<label>优先级</label>
<input
type="number"
@@ -516,7 +516,7 @@ function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps)
disabled={loading}
/>
<span className="form-hint">数字越小优先级越高,范围 1-100</span>
</div>
</div> */}
</form>
</Modal>
);
@@ -632,8 +632,19 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
assignUserRoles(userId, [role.id])
);
await Promise.all(promises);
const results = await Promise.all(promises);
// v3.4: 检查是否有失败的
const failedResults = results.filter(r => !r.success);
if (failedResults.length > 0) {
// 如果有失败的,显示第一个错误消息
const firstError = failedResults[0];
toastService.error(firstError.message);
setLoading(false);
return;
}
// 全部成功
toastService.success(`成功为 ${selectedUserIds.length} 个用户分配角色`);
resetState();
onSuccess();
@@ -733,7 +744,7 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
<span>...</span>
</div>
) : (
<div className="users-checkbox-list">
<div className="users-checkbox-list" style={{ maxHeight: '400px', minHeight: '400px', overflow: 'auto' }}>
{filteredUsers.length > 0 ? (
filteredUsers.map(user => {
const userRoles = userRolesMap.get(user.id) || [];
@@ -778,6 +789,7 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
</div>
<div className="user-meta">
@{user.username} {user.ou_name}
{user.area && `${user.area}`}
{user.phone_number && `${user.phone_number}`}
</div>
</div>
@@ -900,18 +912,19 @@ export default function RolePermissions() {
const filteredRoles = rolesData;
// v3.3: 根据用户地区过滤可见的用户列表
let filteredUsers = usersData;
if (isCityAdmin && currentUserArea) {
// 市级管理员只能看到同地区的用户(使用 area 字段)
filteredUsers = usersData.filter(user =>
user.area === currentUserArea
);
console.log('🔒 [RolePermissions v3.3] 市级管理员用户过滤:', {
当前地区: currentUserArea,
原始用户数: usersData.length,
过滤后用户数: filteredUsers.length
});
}
const filteredUsers = usersData;
// let filteredUsers = usersData;
// if (isCityAdmin && currentUserArea) {
// // 市级管理员只能看到同地区的用户(使用 area 字段)
// filteredUsers = usersData.filter(user =>
// user.area === currentUserArea
// );
// console.log('🔒 [RolePermissions v3.3] 市级管理员用户过滤:', {
// 当前地区: currentUserArea,
// 原始用户数: usersData.length,
// 过滤后用户数: filteredUsers.length
// });
// }
setRoles(filteredRoles);
setRoutes(routesData);
@@ -1507,10 +1520,10 @@ export default function RolePermissions() {
<i className="ri-database-line"></i>
{getDataScopeLabel(role.data_scope)}
</span>
<span className="priority">
{/* <span className="priority">
<i className="ri-sort-asc"></i>
优先级: {role.priority}
</span>
</span> */}
</div>
</div>
{!role.is_system_role && (
@@ -1635,7 +1648,11 @@ export default function RolePermissions() {
)}
</div>
<div className="user-username">@{user.username}</div>
<div className="user-org">{user.ou_name}</div>
<div className="user-org">
{/* {JSON.stringify(user)} */}
{user.ou_name}
{user.area && <span style={{ marginLeft: '8px', color: '#666' }}> {user.area}</span>}
</div>
<div className="user-contact">
{user.phone_number && (
<span>