fix: 1. 全局axios添加formData文件上传的检测,删除Content-Type让axios自动检测。
2. 完善入口模块管理的接口的对接。 3. 完善角色权限管理的接口对接和测试。 4. 完善主页的入口模块的图标的显示和图片的显示。
This commit is contained in:
@@ -80,6 +80,12 @@ function isInErrorTolerantWhitelist(url?: string): boolean {
|
|||||||
*/
|
*/
|
||||||
axiosInstance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
(config) => {
|
(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)) {
|
if (isInAuthWhitelist(config.url)) {
|
||||||
console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url);
|
console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url);
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ interface ListResponse<T> {
|
|||||||
* @param searchParams 搜索参数
|
* @param searchParams 搜索参数
|
||||||
* @returns 入口模块列表和总数
|
* @returns 入口模块列表和总数
|
||||||
*
|
*
|
||||||
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
* 注意:
|
||||||
|
* 1. 不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||||||
|
* 2. 后端返回的 data.items 会被转换为 data.modules(语义化重命名)
|
||||||
*/
|
*/
|
||||||
export async function getEntryModules(
|
export async function getEntryModules(
|
||||||
searchParams: EntryModuleSearchParams = {}
|
searchParams: EntryModuleSearchParams = {}
|
||||||
@@ -254,6 +256,27 @@ export async function createEntryModule(
|
|||||||
* @param module 更新的入口模块数据
|
* @param module 更新的入口模块数据
|
||||||
* @returns 更新的入口模块
|
* @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 读取并添加
|
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||||||
*/
|
*/
|
||||||
export async function updateEntryModule(
|
export async function updateEntryModule(
|
||||||
@@ -324,6 +347,11 @@ export async function updateEntryModule(
|
|||||||
* @param id 入口模块ID
|
* @param id 入口模块ID
|
||||||
* @returns 是否成功
|
* @returns 是否成功
|
||||||
*
|
*
|
||||||
|
* ⚠️ 警告:
|
||||||
|
* 1. 删除操作是物理删除,数据库记录直接删除,不可恢复
|
||||||
|
* 2. MinIO中的图标文件不会自动删除,需要手动清理或通过定时任务清理孤立文件
|
||||||
|
* 3. 如果其他表有外键引用,可能会触发级联删除或报错(取决于外键约束设置)
|
||||||
|
*
|
||||||
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
* 注意:不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||||||
*/
|
*/
|
||||||
export async function deleteEntryModule(
|
export async function deleteEntryModule(
|
||||||
@@ -331,7 +359,7 @@ export async function deleteEntryModule(
|
|||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// 调用 API
|
// 调用 API
|
||||||
const response = await del<ApiResponse<{ id: number; deleted: boolean }>>(
|
const response = await del<ApiResponse<{ message: string }>>(
|
||||||
`/api/v3/entry-modules/${id}`
|
`/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-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 : "上传入口模块图标失败" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -450,7 +450,9 @@ export async function saveRoleApiPermissions(
|
|||||||
data_scope: 'ALL'
|
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,
|
permissions,
|
||||||
replace: true // 替换模式:先删除现有权限,再插入新权限
|
replace: true // 替换模式:先删除现有权限,再插入新权限
|
||||||
});
|
});
|
||||||
@@ -621,8 +623,10 @@ export async function getAllUsers(params?: {
|
|||||||
if (params?.page_size) queryParams.page_size = params.page_size;
|
if (params?.page_size) queryParams.page_size = params.page_size;
|
||||||
if (params?.username) queryParams.search = params.username;
|
if (params?.username) queryParams.search = params.username;
|
||||||
|
|
||||||
// 使用 axios-client 的 get 函数,会自动添加 baseURL 和 Authorization
|
// v3.3: 使用标准 RBAC API,后端会自动根据用户角色进行地区过滤
|
||||||
const response = await get<any>('/admin/users/users', queryParams);
|
// 省级管理员: 返回所有地区用户
|
||||||
|
// 市级管理员: 只返回同地区用户
|
||||||
|
const response = await get<any>('/api/v2/users', queryParams);
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
@@ -643,6 +647,8 @@ export async function getAllUsers(params?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('获取的用户列表', users )
|
||||||
|
|
||||||
|
|
||||||
const userList = users.map(user => ({
|
const userList = users.map(user => ({
|
||||||
id: user.user_id || user.id, // 优先使用 user_id,兼容不同的后端响应格式
|
id: user.user_id || user.id, // 优先使用 user_id,兼容不同的后端响应格式
|
||||||
@@ -685,6 +691,15 @@ export async function assignUserRoles(
|
|||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v3.4: 检查后端返回的code(200表示成功)
|
||||||
|
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: [...] } }
|
// 后端响应格式: { code: 200, message: "角色分配成功", data: { user_id, roles: [...] } }
|
||||||
let message = '用户角色分配成功';
|
let message = '用户角色分配成功';
|
||||||
if (response.data && response.data.message) {
|
if (response.data && response.data.message) {
|
||||||
@@ -995,7 +1010,10 @@ export async function getRolePermissions(roleId: number): Promise<RolePermission
|
|||||||
try {
|
try {
|
||||||
const { get } = await import('~/api/axios-client');
|
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) {
|
if (response.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
@@ -1039,7 +1057,9 @@ export async function assignPermissionsToRole(
|
|||||||
replace = false
|
replace = false
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
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 => ({
|
permissions: permissions.map(p => ({
|
||||||
permission_id: p.permission_id,
|
permission_id: p.permission_id,
|
||||||
grant_type: p.grant_type || 'GRANT',
|
grant_type: p.grant_type || 'GRANT',
|
||||||
@@ -1072,8 +1092,9 @@ export async function updateRolePermission(
|
|||||||
config: Partial<RolePermissionConfig>
|
config: Partial<RolePermissionConfig>
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
|
// v3.4: 使用文档规范的API路径(查询参数方式)
|
||||||
const response = await put<any>(
|
const response = await put<any>(
|
||||||
`${RBAC_API_BASE}/roles/${roleId}/permissions/${permissionId}`,
|
`${RBAC_API_BASE}/role-permissions?role_id=${roleId}&permission_id=${permissionId}`,
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
handleApiResponse<any>(response);
|
handleApiResponse<any>(response);
|
||||||
@@ -1098,7 +1119,8 @@ export async function revokeRolePermission(
|
|||||||
permissionId: number
|
permissionId: number
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
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);
|
handleApiResponse<any>(response);
|
||||||
|
|
||||||
return { success: true, message: '权限移除成功' };
|
return { success: true, message: '权限移除成功' };
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface ButtonProps {
|
|||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
to?: string;
|
to?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
loading?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
@@ -21,6 +22,7 @@ export function Button({
|
|||||||
size = 'medium',
|
size = 'medium',
|
||||||
to,
|
to,
|
||||||
icon,
|
icon,
|
||||||
|
loading = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className = '',
|
className = '',
|
||||||
onClick,
|
onClick,
|
||||||
@@ -44,10 +46,13 @@ export function Button({
|
|||||||
baseClasses,
|
baseClasses,
|
||||||
typeClasses[type],
|
typeClasses[type],
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
disabled ? 'opacity-50 cursor-not-allowed' : '',
|
(disabled || loading) ? 'opacity-50 cursor-not-allowed' : '',
|
||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
// 显示的图标:loading 时显示加载图标,否则显示传入的图标
|
||||||
|
const displayIcon = loading ? 'ri-loader-4-line animate-spin' : icon;
|
||||||
|
|
||||||
if (to) {
|
if (to) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -55,7 +60,7 @@ export function Button({
|
|||||||
className={classes}
|
className={classes}
|
||||||
{...(rest as any)}
|
{...(rest as any)}
|
||||||
>
|
>
|
||||||
{icon && <i className={`${icon} mr-1.5`}></i>}
|
{displayIcon && <i className={`${displayIcon} mr-1.5`}></i>}
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -64,11 +69,11 @@ export function Button({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classes}
|
className={classes}
|
||||||
disabled={disabled}
|
disabled={disabled || loading}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{icon && <i className={`${icon} mr-1.5`}></i>}
|
{displayIcon && <i className={`${displayIcon} mr-1.5`}></i>}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// ✅ 保存权限映射表
|
// ✅ 保存权限映射表
|
||||||
if (routesResult.permissionMap) {
|
if (routesResult.permissionMap) {
|
||||||
permissionMap = routesResult.permissionMap;
|
permissionMap = routesResult.permissionMap;
|
||||||
console.log("🔑 [Root Loader] 权限映射表:", permissionMap);
|
// console.log("🔑 [Root Loader] 权限映射表:", permissionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查当前路径是否在允许列表中
|
// 检查当前路径是否在允许列表中
|
||||||
|
|||||||
+13
-9
@@ -5,6 +5,7 @@ import styles from "~/styles/pages/home.css?url";
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { getUserSession, logout } from "~/api/login/auth.server";
|
import { getUserSession, logout } from "~/api/login/auth.server";
|
||||||
import { toastService } from '~/components/ui';
|
import { toastService } from '~/components/ui';
|
||||||
|
import { DOCUMENT_URL } from '~/config/api-config';
|
||||||
|
|
||||||
export const links = () => [
|
export const links = () => [
|
||||||
{ rel: "stylesheet", href: styles }
|
{ rel: "stylesheet", href: styles }
|
||||||
@@ -227,14 +228,17 @@ export default function Index() {
|
|||||||
// 获取模块图标(根据模块 path 或 id)
|
// 获取模块图标(根据模块 path 或 id)
|
||||||
const getModuleIcon = (module: typeof loaderData.entryModules[0]) => {
|
const getModuleIcon = (module: typeof loaderData.entryModules[0]) => {
|
||||||
// 根据 path 判断图标
|
// 根据 path 判断图标
|
||||||
if (module.path?.includes('ht')) {
|
// if (module.path?.includes('ht')) {
|
||||||
return '/images/icon_hetong.png';
|
// return '/images/icon_hetong.png';
|
||||||
} else if (module.path?.includes('aj')) {
|
// } else if (module.path?.includes('aj')) {
|
||||||
return '/images/icon_anjuan.png';
|
// return '/images/icon_anjuan.png';
|
||||||
} else if (module.path?.includes('nw')) {
|
// } else if (module.path?.includes('nw')) {
|
||||||
return '/images/icon_assistant.png';
|
// return '/images/icon_assistant.png';
|
||||||
}
|
// }
|
||||||
// 默认图标
|
// 默认图标
|
||||||
|
if (module.path){
|
||||||
|
return `${DOCUMENT_URL}${module.path}`
|
||||||
|
}
|
||||||
return '/images/icon_assistant.png';
|
return '/images/icon_assistant.png';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -388,7 +392,7 @@ export default function Index() {
|
|||||||
aria-label="交叉评查"
|
aria-label="交叉评查"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/images/icon_cross_checking.png"
|
src="/images/icon_assistant.png"
|
||||||
alt="交叉评查"
|
alt="交叉评查"
|
||||||
className="w-12 h-12 mx-1"
|
className="w-12 h-12 mx-1"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
@@ -418,7 +422,7 @@ export default function Index() {
|
|||||||
aria-label={module.name}
|
aria-label={module.name}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={getModuleIcon(module)}
|
src={isLLMModule ? '/images/icon_assistant.png' : getModuleIcon(module)}
|
||||||
alt={module.name}
|
alt={module.name}
|
||||||
className="w-12 h-12 mx-1"
|
className="w-12 h-12 mx-1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export default function DocumentTypesList() {
|
|||||||
{
|
{
|
||||||
title: "文档类型名称",
|
title: "文档类型名称",
|
||||||
key: "name",
|
key: "name",
|
||||||
width: "180px",
|
width: "200px",
|
||||||
render: (_: unknown, record: DocumentTypeUI) => (
|
render: (_: unknown, record: DocumentTypeUI) => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<i className="ri-file-text-line text-primary mr-2"></i>
|
<i className="ri-file-text-line text-primary mr-2"></i>
|
||||||
@@ -454,7 +454,7 @@ export default function DocumentTypesList() {
|
|||||||
placeholder="请输入文档类型名称"
|
placeholder="请输入文档类型名称"
|
||||||
value={name}
|
value={name}
|
||||||
onSearch={handleNameSearch}
|
onSearch={handleNameSearch}
|
||||||
className="flex-1 min-w-[200px]"
|
className="min-w-[400px]"
|
||||||
instantSearch={true}
|
instantSearch={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ export default function EntryModulesList() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const revalidator = useRevalidator();
|
const revalidator = useRevalidator();
|
||||||
|
|
||||||
|
// 记录加载失败的图片ID
|
||||||
|
const [failedImages, setFailedImages] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// 获取加载器数据
|
// 获取加载器数据
|
||||||
const loaderData = useLoaderData<LoaderData>();
|
const loaderData = useLoaderData<LoaderData>();
|
||||||
const { modules, total, error } = loaderData;
|
const { modules, total, error } = loaderData;
|
||||||
@@ -259,26 +262,41 @@ export default function EntryModulesList() {
|
|||||||
if (!record.path) {
|
if (!record.path) {
|
||||||
return <span className="text-gray-400">未上传</span>;
|
return <span className="text-gray-400">未上传</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoUrl = `${DOCUMENT_URL}${record.path}`;
|
const logoUrl = `${DOCUMENT_URL}${record.path}`;
|
||||||
|
const hasFailed = failedImages.has(record.id!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
{!hasFailed ? (
|
||||||
src={logoUrl}
|
<>
|
||||||
alt={record.name}
|
<a
|
||||||
className="h-8 w-8 object-contain rounded"
|
href={logoUrl}
|
||||||
onError={(e) => {
|
target="_blank"
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
rel="noopener noreferrer"
|
||||||
(e.target as HTMLImageElement).parentElement!.innerHTML = '<span class="text-red-500">加载失败</span>';
|
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">
|
||||||
<a
|
<img
|
||||||
href={logoUrl}
|
src={logoUrl}
|
||||||
target="_blank"
|
alt={record.name}
|
||||||
rel="noopener noreferrer"
|
className="h-full w-full object-contain"
|
||||||
className="ml-2 text-blue-600 hover:underline text-sm"
|
onError={() => {
|
||||||
>
|
// 使用 React 状态管理加载失败的图片
|
||||||
查看
|
setFailedImages(prev => new Set(prev).add(record.id!));
|
||||||
</a>
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -396,10 +414,11 @@ export default function EntryModulesList() {
|
|||||||
{/* 分页 */}
|
{/* 分页 */}
|
||||||
{total > 0 && (
|
{total > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
current={currentPage}
|
// pageSizeOptions={[10,20]}
|
||||||
|
currentPage={currentPage}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
total={total}
|
total={total}
|
||||||
onPageChange={handlePageChange}
|
onChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate, useSearchParams, useLoaderData } from "@remix-run/react";
|
import { useNavigate, useSearchParams, useLoaderData, ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
|
||||||
import { ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
|
|
||||||
import { Card } from "~/components/ui/Card";
|
import { Card } from "~/components/ui/Card";
|
||||||
import { Button } from "~/components/ui/Button";
|
import { Button } from "~/components/ui/Button";
|
||||||
import { toastService } from "~/components/ui/Toast";
|
import { toastService } from "~/components/ui/Toast";
|
||||||
import { Modal } from "~/components/ui/Modal";
|
|
||||||
import { usePermission } from "~/hooks/usePermission";
|
import { usePermission } from "~/hooks/usePermission";
|
||||||
import {
|
import {
|
||||||
getEntryModuleById,
|
getEntryModuleById,
|
||||||
createEntryModule,
|
createEntryModule,
|
||||||
updateEntryModule,
|
updateEntryModule,
|
||||||
|
uploadEntryModuleImage,
|
||||||
type EntryModule,
|
type EntryModule,
|
||||||
type AreaConfig
|
type AreaConfig
|
||||||
} from "~/api/entry-modules/entry-modules";
|
} 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";
|
import entryModulesStyles from "~/styles/pages/entry-modules.css?url";
|
||||||
|
|
||||||
// 引入CSS样式
|
// 引入CSS样式
|
||||||
@@ -110,7 +109,6 @@ export default function EntryModuleNew() {
|
|||||||
module?.path ? `${DOCUMENT_URL}${module.path}` : null
|
module?.path ? `${DOCUMENT_URL}${module.path}` : null
|
||||||
);
|
);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -185,48 +183,6 @@ export default function EntryModuleNew() {
|
|||||||
return true;
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
// ✅ Runtime permission check
|
// ✅ Runtime permission check
|
||||||
@@ -245,27 +201,26 @@ export default function EntryModuleNew() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 上传logo
|
// 🔑 步骤1: 先创建或更新模块(不包含图片上传)
|
||||||
let logoPath = module?.path || null;
|
|
||||||
if (logoFile) {
|
|
||||||
logoPath = await uploadLogo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔑 准备提交数据
|
|
||||||
// areas 字段会在 API 层自动转换为 AreaConfig[] 格式
|
// areas 字段会在 API 层自动转换为 AreaConfig[] 格式
|
||||||
const moduleData = {
|
const moduleData = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
path: logoPath,
|
// 创建时 path 为 null,编辑时保持原 path(图片上传接口会自动更新)
|
||||||
|
path: isEditMode ? module?.path : null,
|
||||||
areas: selectedAreas // 字符串数组,API会自动转换
|
areas: selectedAreas // 字符串数组,API会自动转换
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ 不需要传递 JWT,axios-client 会自动处理
|
// ✅ 不需要传递 JWT,axios-client 会自动处理
|
||||||
let result;
|
let result;
|
||||||
|
let moduleId: number;
|
||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
result = await updateEntryModule(parseInt(id!), moduleData);
|
result = await updateEntryModule(parseInt(id!), moduleData);
|
||||||
|
moduleId = parseInt(id!);
|
||||||
} else {
|
} else {
|
||||||
result = await createEntryModule(moduleData);
|
result = await createEntryModule(moduleData);
|
||||||
|
moduleId = result.data!.id!;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@@ -273,6 +228,19 @@ export default function EntryModuleNew() {
|
|||||||
return;
|
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 ? '更新成功!' : '创建成功!');
|
toastService.success(isEditMode ? '更新成功!' : '创建成功!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/entry-modules');
|
navigate('/entry-modules');
|
||||||
@@ -287,14 +255,10 @@ export default function EntryModuleNew() {
|
|||||||
|
|
||||||
// 处理取消
|
// 处理取消
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setShowConfirmModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确认取消
|
|
||||||
const confirmCancel = () => {
|
|
||||||
navigate('/entry-modules');
|
navigate('/entry-modules');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="entry-modules-new-page">
|
<div className="entry-modules-new-page">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -429,33 +393,6 @@ export default function EntryModuleNew() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ export function links() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 面包屑导航
|
// 面包屑导航
|
||||||
// export const handle = {
|
export const handle = {
|
||||||
// breadcrumb: () => {
|
breadcrumb: "文档列表",
|
||||||
// return '上传文件'
|
previousRoute: {
|
||||||
// }
|
title: "文档列表",
|
||||||
// }
|
to: "/documents/list"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const meta = () => {
|
|||||||
function getDataScopeLabel(scope: string): string {
|
function getDataScopeLabel(scope: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'ALL': '全部数据',
|
'ALL': '全部数据',
|
||||||
'DEPT': '部门数据',
|
'DEPT': '地市级数据',
|
||||||
'SELF': '仅本人数据'
|
'SELF': '仅本人数据'
|
||||||
};
|
};
|
||||||
return map[scope] || scope;
|
return map[scope] || scope;
|
||||||
@@ -314,13 +314,13 @@ function CreateRoleModal({ isOpen, onClose, onSuccess }: CreateRoleModalProps) {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<option value="SELF">仅本人数据</option>
|
<option value="SELF">仅本人数据</option>
|
||||||
<option value="DEPT">部门数据</option>
|
<option value="DEPT">地市级数据</option>
|
||||||
<option value="ALL">全部数据</option>
|
<option value="ALL">全部数据</option>
|
||||||
</select>
|
</select>
|
||||||
<span className="form-hint">控制该角色可访问的数据范围</span>
|
<span className="form-hint">控制该角色可访问的数据范围</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
{/* <div className="form-group">
|
||||||
<label>优先级</label>
|
<label>优先级</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -333,7 +333,7 @@ function CreateRoleModal({ isOpen, onClose, onSuccess }: CreateRoleModalProps) {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<span className="form-hint">数字越小优先级越高,范围 1-100</span>
|
<span className="form-hint">数字越小优先级越高,范围 1-100</span>
|
||||||
</div>
|
</div> */}
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -495,7 +495,7 @@ function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps)
|
|||||||
disabled={loading || (role.is_system_role && role.role_key === 'provincial_admin')}
|
disabled={loading || (role.is_system_role && role.role_key === 'provincial_admin')}
|
||||||
>
|
>
|
||||||
<option value="SELF">仅本人数据</option>
|
<option value="SELF">仅本人数据</option>
|
||||||
<option value="DEPT">部门数据</option>
|
<option value="DEPT">地市级数据</option>
|
||||||
<option value="ALL">全部数据</option>
|
<option value="ALL">全部数据</option>
|
||||||
</select>
|
</select>
|
||||||
{role.is_system_role && role.role_key === 'provincial_admin' && (
|
{role.is_system_role && role.role_key === 'provincial_admin' && (
|
||||||
@@ -503,7 +503,7 @@ function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps)
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
{/* <div className="form-group">
|
||||||
<label>优先级</label>
|
<label>优先级</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -516,7 +516,7 @@ function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps)
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<span className="form-hint">数字越小优先级越高,范围 1-100</span>
|
<span className="form-hint">数字越小优先级越高,范围 1-100</span>
|
||||||
</div>
|
</div> */}
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -632,8 +632,19 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
|||||||
assignUserRoles(userId, [role.id])
|
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} 个用户分配角色`);
|
toastService.success(`成功为 ${selectedUserIds.length} 个用户分配角色`);
|
||||||
resetState();
|
resetState();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
@@ -733,7 +744,7 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
|||||||
<span>加载用户中...</span>
|
<span>加载用户中...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="users-checkbox-list">
|
<div className="users-checkbox-list" style={{ maxHeight: '400px', minHeight: '400px', overflow: 'auto' }}>
|
||||||
{filteredUsers.length > 0 ? (
|
{filteredUsers.length > 0 ? (
|
||||||
filteredUsers.map(user => {
|
filteredUsers.map(user => {
|
||||||
const userRoles = userRolesMap.get(user.id) || [];
|
const userRoles = userRolesMap.get(user.id) || [];
|
||||||
@@ -778,6 +789,7 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
|||||||
</div>
|
</div>
|
||||||
<div className="user-meta">
|
<div className="user-meta">
|
||||||
@{user.username} • {user.ou_name}
|
@{user.username} • {user.ou_name}
|
||||||
|
{user.area && ` • ${user.area}`}
|
||||||
{user.phone_number && ` • ${user.phone_number}`}
|
{user.phone_number && ` • ${user.phone_number}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -900,18 +912,19 @@ export default function RolePermissions() {
|
|||||||
const filteredRoles = rolesData;
|
const filteredRoles = rolesData;
|
||||||
|
|
||||||
// v3.3: 根据用户地区过滤可见的用户列表
|
// v3.3: 根据用户地区过滤可见的用户列表
|
||||||
let filteredUsers = usersData;
|
const filteredUsers = usersData;
|
||||||
if (isCityAdmin && currentUserArea) {
|
// let filteredUsers = usersData;
|
||||||
// 市级管理员只能看到同地区的用户(使用 area 字段)
|
// if (isCityAdmin && currentUserArea) {
|
||||||
filteredUsers = usersData.filter(user =>
|
// // 市级管理员只能看到同地区的用户(使用 area 字段)
|
||||||
user.area === currentUserArea
|
// filteredUsers = usersData.filter(user =>
|
||||||
);
|
// user.area === currentUserArea
|
||||||
console.log('🔒 [RolePermissions v3.3] 市级管理员用户过滤:', {
|
// );
|
||||||
当前地区: currentUserArea,
|
// console.log('🔒 [RolePermissions v3.3] 市级管理员用户过滤:', {
|
||||||
原始用户数: usersData.length,
|
// 当前地区: currentUserArea,
|
||||||
过滤后用户数: filteredUsers.length
|
// 原始用户数: usersData.length,
|
||||||
});
|
// 过滤后用户数: filteredUsers.length
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
setRoles(filteredRoles);
|
setRoles(filteredRoles);
|
||||||
setRoutes(routesData);
|
setRoutes(routesData);
|
||||||
@@ -1507,10 +1520,10 @@ export default function RolePermissions() {
|
|||||||
<i className="ri-database-line"></i>
|
<i className="ri-database-line"></i>
|
||||||
{getDataScopeLabel(role.data_scope)}
|
{getDataScopeLabel(role.data_scope)}
|
||||||
</span>
|
</span>
|
||||||
<span className="priority">
|
{/* <span className="priority">
|
||||||
<i className="ri-sort-asc"></i>
|
<i className="ri-sort-asc"></i>
|
||||||
优先级: {role.priority}
|
优先级: {role.priority}
|
||||||
</span>
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!role.is_system_role && (
|
{!role.is_system_role && (
|
||||||
@@ -1635,7 +1648,11 @@ export default function RolePermissions() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="user-username">@{user.username}</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">
|
<div className="user-contact">
|
||||||
{user.phone_number && (
|
{user.phone_number && (
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
Reference in New Issue
Block a user