Merge branch 'Wren' into shiy-login
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+768
-327
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,26 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { EvaluationPoint } from '~/models/evaluation_points';
|
||||
import type { EvaluationPointGroup } from '~/models/evaluation_point_groups';
|
||||
import { getRulesList } from '~/api/evaluation_points/rules';
|
||||
|
||||
interface BasicInfoProps {
|
||||
onChange?: (data: Record<string, unknown>) => void;
|
||||
initialData?: EvaluationPoint;
|
||||
evaluationPointGroups?: EvaluationPointGroup[];
|
||||
riskOptions?: Array<{value: string, label: string}>;
|
||||
frontendJWT?: string;
|
||||
evaluationPointId?: number | string;
|
||||
}
|
||||
|
||||
// 评查点基本信息组件
|
||||
export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], riskOptions = [] }: BasicInfoProps) {
|
||||
export function BasicInfo({
|
||||
onChange,
|
||||
initialData,
|
||||
evaluationPointGroups = [],
|
||||
riskOptions = [],
|
||||
frontendJWT,
|
||||
evaluationPointId
|
||||
}: BasicInfoProps) {
|
||||
const [formData, setFormData] = useState<EvaluationPoint>({
|
||||
risk: 'medium', // 风险等级 默认中风险
|
||||
is_enabled: true, // 是否启用 默认启用
|
||||
@@ -21,6 +32,47 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
|
||||
...(initialData || {}) // 合并初始数据
|
||||
});
|
||||
|
||||
// 编码验证状态
|
||||
const [codeValidating, setCodeValidating] = useState(false);
|
||||
const [codeError, setCodeError] = useState('');
|
||||
const [codeValidationTimer, setCodeValidationTimer] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 异步验证编码唯一性
|
||||
const validateCodeUnique = async (code: string): Promise<string> => {
|
||||
if (!code.trim()) {
|
||||
return ''; // 空值不验证
|
||||
}
|
||||
|
||||
setCodeValidating(true);
|
||||
setCodeError('');
|
||||
|
||||
try {
|
||||
const response = await getRulesList({
|
||||
keyword: code.trim(),
|
||||
pageSize: 10,
|
||||
token: frontendJWT
|
||||
});
|
||||
|
||||
if (response.data && response.data.rules && response.data.rules.length > 0) {
|
||||
// 检查是否有完全匹配的编码(排除当前编辑的评查点)
|
||||
const isDuplicate = response.data.rules.some(rule =>
|
||||
rule.code === code.trim() && String(rule.id) !== String(evaluationPointId)
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
return '该编码已被使用,请使用其他编码';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
} catch (error) {
|
||||
console.error('验证编码唯一性失败:', error);
|
||||
return ''; // 验证失败不阻止用户输入
|
||||
} finally {
|
||||
setCodeValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 找到当前评查点类型对应的code
|
||||
const getCheckpointTypeCode = () => {
|
||||
if (!formData.evaluation_point_groups_pid) return "";
|
||||
@@ -80,11 +132,23 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
|
||||
const newData = { ...formData };
|
||||
// 映射id到表单字段名
|
||||
switch(id) {
|
||||
case 'rule-name':
|
||||
case 'rule-name':
|
||||
newData.name = value;
|
||||
break;
|
||||
case 'rule-code':
|
||||
case 'rule-code':
|
||||
newData.code = value;
|
||||
// 清除之前的验证定时器
|
||||
if (codeValidationTimer) {
|
||||
clearTimeout(codeValidationTimer);
|
||||
}
|
||||
// 清除错误信息
|
||||
setCodeError('');
|
||||
// 设置新的验证定时器(500ms后触发验证)
|
||||
const timer = setTimeout(async () => {
|
||||
const error = await validateCodeUnique(value);
|
||||
setCodeError(error);
|
||||
}, 500);
|
||||
setCodeValidationTimer(timer);
|
||||
break;
|
||||
case 'risk-level':
|
||||
newData.risk = value;
|
||||
@@ -197,6 +261,15 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
|
||||
// }
|
||||
// }, [filteredRuleGroups, onChange]);
|
||||
|
||||
// 清理验证定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (codeValidationTimer) {
|
||||
clearTimeout(codeValidationTimer);
|
||||
}
|
||||
};
|
||||
}, [codeValidationTimer]);
|
||||
|
||||
return (
|
||||
<div className="ant-card">
|
||||
<div className="ant-card-header">
|
||||
@@ -221,16 +294,21 @@ export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], r
|
||||
<div>
|
||||
<label className="form-label" htmlFor="rule-code">
|
||||
评查点编码 <span className="required-mark">*</span>
|
||||
{codeValidating && <span className="ml-2 text-sm text-gray-500">验证中...</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="rule-code"
|
||||
className="form-input"
|
||||
className={`form-input ${codeError ? 'border-red-500' : ''}`}
|
||||
placeholder="请输入评查点编码"
|
||||
value={formData.code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<div className="form-tip">用于系统标识的唯一编码</div>
|
||||
{codeError ? (
|
||||
<div className="form-tip text-red-500">{codeError}</div>
|
||||
) : (
|
||||
<div className="form-tip">用于系统标识的唯一编码</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="risk-level">
|
||||
|
||||
@@ -40,6 +40,8 @@ interface MessageModalProps {
|
||||
customIcon?: React.ReactNode;
|
||||
// 自定义内容
|
||||
children?: React.ReactNode;
|
||||
// 确认按钮延迟时间(秒)- 用于危险操作(如删除)
|
||||
confirmDelay?: number;
|
||||
}
|
||||
|
||||
// 默认自动关闭延迟
|
||||
@@ -63,10 +65,12 @@ export function MessageModal({
|
||||
cancelText = '取消',
|
||||
showCloseButton = true,
|
||||
customIcon,
|
||||
children
|
||||
children,
|
||||
confirmDelay = 0
|
||||
}: MessageModalProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(confirmDelay);
|
||||
|
||||
// 在客户端渲染时获取 portal 容器
|
||||
useEffect(() => {
|
||||
@@ -108,6 +112,23 @@ export function MessageModal({
|
||||
}
|
||||
}, [isOpen, autoClose, autoCloseDelay, handleClose]);
|
||||
|
||||
// 确认延迟倒计时
|
||||
useEffect(() => {
|
||||
if (isOpen && confirmDelay > 0) {
|
||||
setRemainingSeconds(confirmDelay);
|
||||
const timer = setInterval(() => {
|
||||
setRemainingSeconds((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
}, [isOpen, confirmDelay]);
|
||||
|
||||
// 关闭按钮键盘交互
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -194,15 +215,20 @@ export function MessageModal({
|
||||
<div className="message-modal-actions">
|
||||
{onConfirm && (
|
||||
<>
|
||||
<button
|
||||
className="message-modal-button primary"
|
||||
<button
|
||||
className="message-modal-button primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={remainingSeconds > 0}
|
||||
style={{
|
||||
opacity: remainingSeconds > 0 ? 0.5 : 1,
|
||||
cursor: remainingSeconds > 0 ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{confirmText}
|
||||
{remainingSeconds > 0 ? `${confirmText} (${remainingSeconds}s)` : confirmText}
|
||||
</button>
|
||||
{cancelText && (
|
||||
<button
|
||||
className="message-modal-button"
|
||||
<button
|
||||
className="message-modal-button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{cancelText}
|
||||
@@ -210,10 +236,10 @@ export function MessageModal({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{!onConfirm && (
|
||||
<button
|
||||
className="message-modal-button primary"
|
||||
<button
|
||||
className="message-modal-button primary"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{confirmText}
|
||||
|
||||
@@ -25,7 +25,7 @@ export { UploadArea } from './UploadArea';
|
||||
|
||||
// 反馈组件
|
||||
export { Alert } from './Alert';
|
||||
export { MessageModal } from './MessageModal';
|
||||
export { MessageModal, messageService } from './MessageModal';
|
||||
export { LoadingBar } from './LoadingBar';
|
||||
export { RouteChangeLoader } from './RouteChangeLoader';
|
||||
export { FileProgress } from './FileProgress';
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* RBAC API 代理 - 单个角色操作
|
||||
* GET /api/v3/rbac/roles/:roleId - 获取角色详情
|
||||
* PUT /api/v3/rbac/roles/:roleId - 更新角色
|
||||
* DELETE /api/v3/rbac/roles/:roleId - 删除角色
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { mockRoles, updateRole as updateMockRole, deleteRole as deleteMockRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// GET - 获取角色详情
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
console.log('📡 [API Route] GET /api/v3/rbac/roles/' + roleId);
|
||||
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
|
||||
if (!role) {
|
||||
return json({
|
||||
detail: '角色不存在'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: role
|
||||
});
|
||||
}
|
||||
|
||||
// PUT/DELETE
|
||||
export async function action({ request, params }: LoaderFunctionArgs) {
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
const method = request.method;
|
||||
|
||||
console.log('📡 [API Route]', method, '/api/v3/rbac/roles/' + roleId);
|
||||
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
|
||||
if (!role) {
|
||||
return json({
|
||||
detail: '角色不存在'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
if (method === 'PUT') {
|
||||
// 更新角色
|
||||
const body = await request.json();
|
||||
console.log('📋 [API Route] 更新数据:', body);
|
||||
|
||||
// 系统角色保护
|
||||
if (role.is_system && body.role_key) {
|
||||
return json({
|
||||
detail: '系统角色的role_key不可修改'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 使用共享Mock数据更新
|
||||
updateMockRole(roleId, body);
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色更新成功',
|
||||
data: role
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
// 删除角色
|
||||
if (role.is_system) {
|
||||
return json({
|
||||
detail: '系统角色不能删除'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 使用共享Mock数据删除
|
||||
deleteMockRole(roleId);
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色删除成功'
|
||||
});
|
||||
}
|
||||
|
||||
return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 });
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* RBAC API 代理 - 角色用户管理
|
||||
* GET /api/v3/rbac/roles/:roleId/users - 获取角色的用户列表
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { mockUsers, mockUserRoles, getRoleUsers } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// GET - 获取角色的用户列表
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
console.log('📡 [API Route] GET /api/v3/rbac/roles/' + roleId + '/users');
|
||||
|
||||
// 解析查询参数
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '20');
|
||||
const area = url.searchParams.get('area');
|
||||
const username = url.searchParams.get('username');
|
||||
|
||||
// 使用共享Mock数据获取角色用户
|
||||
let users = getRoleUsers(roleId);
|
||||
|
||||
// 过滤
|
||||
if (area) {
|
||||
users = users.filter(u => u.area.includes(area));
|
||||
}
|
||||
|
||||
if (username) {
|
||||
users = users.filter(u =>
|
||||
u.username.includes(username) || u.nick_name.includes(username)
|
||||
);
|
||||
}
|
||||
|
||||
// 添加assigned_at时间戳
|
||||
const usersWithTime = users.map(user => {
|
||||
const userRole = mockUserRoles.find(
|
||||
ur => ur.user_id === (user.user_id || user.id) && ur.role_id === roleId
|
||||
);
|
||||
return {
|
||||
...user,
|
||||
user_id: user.user_id || user.id,
|
||||
assigned_at: userRole?.assigned_at || new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
// 分页
|
||||
const total = usersWithTime.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const items = usersWithTime.slice(start, end);
|
||||
|
||||
console.log('✅ [API Route] 返回用户数据:', { roleId, total, itemsCount: items.length });
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
items
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* RBAC API 代理 - 获取角色列表
|
||||
* GET /api/v3/rbac/roles
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { mockRoles, mockUserRoles, addRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// GET - 获取角色列表
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
console.log('📡 [API Route] GET /api/v3/rbac/roles 被调用');
|
||||
|
||||
// 解析查询参数
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '20');
|
||||
const roleKey = url.searchParams.get('role_key');
|
||||
const roleName = url.searchParams.get('role_name');
|
||||
|
||||
console.log('📋 [API Route] 查询参数:', { page, pageSize, roleKey, roleName });
|
||||
|
||||
// 过滤数据
|
||||
let filteredRoles = [...mockRoles];
|
||||
|
||||
if (roleKey) {
|
||||
filteredRoles = filteredRoles.filter(r => r.role_key.includes(roleKey));
|
||||
}
|
||||
|
||||
if (roleName) {
|
||||
filteredRoles = filteredRoles.filter(r => r.role_name.includes(roleName));
|
||||
}
|
||||
|
||||
// 添加用户数统计
|
||||
const rolesWithCount = filteredRoles.map(role => ({
|
||||
...role,
|
||||
user_count: mockUserRoles.filter(ur => ur.role_id === role.id).length,
|
||||
permission_count: 0 // 暂时写死
|
||||
}));
|
||||
|
||||
// 分页
|
||||
const total = rolesWithCount.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const items = rolesWithCount.slice(start, end);
|
||||
|
||||
// 返回标准格式
|
||||
const response = {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
items
|
||||
}
|
||||
};
|
||||
|
||||
console.log('✅ [API Route] 返回数据:', { total, itemsCount: items.length });
|
||||
|
||||
return json(response, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// POST - 创建角色
|
||||
export async function action({ request }: LoaderFunctionArgs) {
|
||||
const method = request.method;
|
||||
|
||||
if (method === 'POST') {
|
||||
console.log('📡 [API Route] POST /api/v3/rbac/roles 被调用');
|
||||
|
||||
const body = await request.json();
|
||||
console.log('📋 [API Route] 请求体:', body);
|
||||
|
||||
// 使用共享Mock数据
|
||||
const newRole = addRole({
|
||||
role_key: body.role_key,
|
||||
role_name: body.role_name,
|
||||
description: body.description || '',
|
||||
data_scope: body.data_scope || 'SELF',
|
||||
priority: body.priority || 10,
|
||||
is_system: false
|
||||
});
|
||||
|
||||
console.log('✅ [API Route] 角色创建成功:', newRole);
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色创建成功',
|
||||
data: newRole
|
||||
});
|
||||
}
|
||||
|
||||
return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 });
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* RBAC API 代理 - 移除用户角色
|
||||
* DELETE /api/v3/rbac/users/:userId/roles/:roleId
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { removeUserRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// DELETE - 移除用户角色
|
||||
export async function action({ params }: LoaderFunctionArgs) {
|
||||
const userId = parseInt(params.userId || '0');
|
||||
const roleId = parseInt(params.roleId || '0');
|
||||
|
||||
console.log('📡 [API Route] DELETE /api/v3/rbac/users/' + userId + '/roles/' + roleId);
|
||||
|
||||
// 使用共享Mock数据移除角色
|
||||
const success = removeUserRole(userId, roleId);
|
||||
|
||||
if (success) {
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '用户角色移除成功'
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
detail: '用户角色关联不存在'
|
||||
}, { status: 404 });
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* RBAC API 代理 - 用户角色管理
|
||||
* POST /api/v3/rbac/users/:userId/roles - 为用户分配角色
|
||||
*/
|
||||
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { assignUserRole } from "~/services/rbac-mock-data.server";
|
||||
|
||||
// POST - 为用户分配角色
|
||||
export async function action({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = parseInt(params.userId || '0');
|
||||
const method = request.method;
|
||||
|
||||
console.log('📡 [API Route]', method, '/api/v3/rbac/users/' + userId + '/roles');
|
||||
|
||||
if (method === 'POST') {
|
||||
const body = await request.json();
|
||||
const roleIds = body.role_ids || [];
|
||||
|
||||
console.log('📋 [API Route] 分配角色:', { userId, roleIds });
|
||||
|
||||
// 使用共享Mock数据分配角色
|
||||
roleIds.forEach((roleId: number) => {
|
||||
assignUserRole(userId, roleId);
|
||||
});
|
||||
|
||||
return json({
|
||||
code: 200,
|
||||
message: '角色分配成功',
|
||||
data: {
|
||||
user_id: userId,
|
||||
roles: roleIds.map((id: number) => ({ role_id: id }))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 });
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Button } from "~/components/ui/Button";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
@@ -201,34 +202,42 @@ export default function DocumentTypesList() {
|
||||
|
||||
// 处理删除文档类型
|
||||
const handleDelete = async (id: number) => {
|
||||
if (confirm('确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。')) {
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id.toString());
|
||||
formData.append('intent', 'delete');
|
||||
|
||||
const response = await fetch('/document-types?index', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(`删除失败: ${result.error || '未知错误'}`);
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。",
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id.toString());
|
||||
formData.append('intent', 'delete');
|
||||
|
||||
const response = await fetch('/document-types?index', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toastService.success('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(`删除失败: ${result.error || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toastService.error(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理编辑文档类型
|
||||
|
||||
@@ -576,21 +576,22 @@ export default function DocumentsIndex() {
|
||||
toastService.warning("文件正在处理中,无法删除");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: `确定要删除文档"${name}"吗?`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: () => {
|
||||
// 设置删除状态为true
|
||||
setIsDeleting(true);
|
||||
|
||||
|
||||
const form = new FormData();
|
||||
form.append("_action", "delete");
|
||||
form.append("id", id);
|
||||
|
||||
|
||||
fetcher.submit(form, { method: "post" });
|
||||
}
|
||||
});
|
||||
@@ -602,38 +603,39 @@ export default function DocumentsIndex() {
|
||||
toastService.error('请至少选择一个文档');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 检查是否有正在处理中的文件
|
||||
const hasProcessingFiles = documents.some((doc: DocumentUI) =>
|
||||
const hasProcessingFiles = documents.some((doc: DocumentUI) =>
|
||||
selectedRowKeys.includes(doc.id.toString()) && doc.fileStatus !== 'Processed'
|
||||
);
|
||||
|
||||
|
||||
if (hasProcessingFiles) {
|
||||
toastService.error('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
messageService.show({
|
||||
title: "确认批量删除",
|
||||
message: `确认删除选中的 ${selectedRowKeys.length} 个文档?`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: () => {
|
||||
// 设置删除状态为true
|
||||
setIsDeleting(true);
|
||||
|
||||
|
||||
// 使用fetcher提交表单
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'batchDelete');
|
||||
|
||||
|
||||
// 添加所有选中的ID
|
||||
selectedRowKeys.forEach(id => {
|
||||
formData.append('ids', id);
|
||||
});
|
||||
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
|
||||
|
||||
// 清空选中行
|
||||
setSelectedRowKeys([]);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "~/components/ui/Button";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import {
|
||||
getEntryModules,
|
||||
deleteEntryModule,
|
||||
@@ -211,34 +212,42 @@ export default function EntryModulesList() {
|
||||
|
||||
// 处理删除入口模块
|
||||
const handleDelete = async (id: number) => {
|
||||
if (confirm('确定要删除该入口模块吗?此操作不可撤销。')) {
|
||||
setIsDeleting(true);
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该入口模块吗?此操作不可撤销。",
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id.toString());
|
||||
formData.append('intent', 'delete');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id.toString());
|
||||
formData.append('intent', 'delete');
|
||||
|
||||
const response = await fetch('/entry-modules', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const response = await fetch('/entry-modules', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toastService.success('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(`删除失败: ${result.error || '未知错误'}`);
|
||||
if (result.success) {
|
||||
toastService.success('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(`删除失败: ${result.error || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toastService.error(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
} catch (error) {
|
||||
toastService.error(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理编辑入口模块
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterP
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
import { toastService } from "~/components/ui";
|
||||
import { toastService, messageService } from "~/components/ui";
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
@@ -217,13 +217,21 @@ export default function PromptsIndex() {
|
||||
|
||||
// 删除模板
|
||||
const handleDeleteTemplate = (id: string) => {
|
||||
if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('intent', 'delete');
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该模板吗?删除后无法恢复。",
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: () => {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('intent', 'delete');
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
}
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听 fetcher 状态变化
|
||||
|
||||
@@ -8,8 +8,15 @@ import { StatusDot } from "~/components/ui/StatusDot";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
// import { Pagination } from "~/components/ui/Pagination";
|
||||
import { getRuleGroups, getChildGroups, type RuleGroup, deleteRuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import { toastService } from "~/components/ui";
|
||||
import {
|
||||
getRuleGroups,
|
||||
getChildGroups,
|
||||
type RuleGroup,
|
||||
deleteRuleGroup,
|
||||
batchUpdateRuleGroupStatus,
|
||||
batchDeleteRuleGroups
|
||||
} from "~/api/evaluation_points/rule-groups";
|
||||
import { toastService, messageService } from "~/components/ui";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: indexStyles }];
|
||||
@@ -27,20 +34,51 @@ export async function loader({ request }: { request: Request }) {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const response = await getRuleGroups(frontendJWT);
|
||||
|
||||
// 🆕 解析URL查询参数(服务端筛选和分页)
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const code = url.searchParams.get('code') || undefined;
|
||||
const is_enabled = url.searchParams.get('is_enabled');
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '50');
|
||||
|
||||
// 🆕 调用增强的 getRuleGroups API
|
||||
const response = await getRuleGroups({
|
||||
name,
|
||||
code,
|
||||
is_enabled: is_enabled ? is_enabled === 'true' : undefined,
|
||||
pid: null, // 仅获取一级分组
|
||||
page,
|
||||
pageSize,
|
||||
token: frontendJWT
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
return Response.json({ groups: response.data, frontendJWT });
|
||||
|
||||
return Response.json({
|
||||
groups: response.data || [],
|
||||
totalCount: ('totalCount' in response) ? (response.totalCount || 0) : 0,
|
||||
page,
|
||||
pageSize,
|
||||
frontendJWT
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载评查点分组失败:', error);
|
||||
return Response.json({ groups: [] });
|
||||
return Response.json({
|
||||
groups: [],
|
||||
totalCount: 0,
|
||||
page: 1,
|
||||
pageSize: 50
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function RuleGroupsIndex() {
|
||||
const { groups: initialGroups, frontendJWT } = useLoaderData<typeof loader>();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { groups: initialGroups, totalCount = 0, page = 1, pageSize = 50, frontendJWT } = loaderData;
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -49,6 +87,7 @@ export default function RuleGroupsIndex() {
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
|
||||
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]); // 🆕 批量选择状态
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
@@ -191,42 +230,118 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 处理删除分组
|
||||
const handleDeleteGroup = async (groupId: string) => {
|
||||
if (confirm("确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。")) {
|
||||
try {
|
||||
const result = await deleteRuleGroup(groupId, frontendJWT);
|
||||
if (result.success) {
|
||||
// 从本地状态中移除被删除的分组
|
||||
setGroups(prev => {
|
||||
// 如果是一级分组,直接过滤掉
|
||||
const filteredGroups = prev.filter(g => g.id !== groupId);
|
||||
|
||||
// 如果是二级分组,需要从父分组的 children 中移除
|
||||
return filteredGroups.map(group => {
|
||||
if (group.children) {
|
||||
return {
|
||||
...group,
|
||||
children: group.children.filter(child => child.id !== groupId)
|
||||
};
|
||||
}
|
||||
return group;
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。",
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const result = await deleteRuleGroup(groupId, frontendJWT);
|
||||
if (result.success) {
|
||||
// 从本地状态中移除被删除的分组
|
||||
setGroups(prev => {
|
||||
// 如果是一级分组,直接过滤掉
|
||||
const filteredGroups = prev.filter(g => g.id !== groupId);
|
||||
|
||||
// 如果是二级分组,需要从父分组的 children 中移除
|
||||
return filteredGroups.map(group => {
|
||||
if (group.children) {
|
||||
return {
|
||||
...group,
|
||||
children: group.children.filter(child => child.id !== groupId)
|
||||
};
|
||||
}
|
||||
return group;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 如果被删除的分组当前是展开状态,从展开列表中移除
|
||||
setExpandedGroups(prev => prev.filter(id => id !== groupId));
|
||||
|
||||
// 显示成功消息
|
||||
// alert('删除成功');
|
||||
toastService.success('删除成功')
|
||||
} else {
|
||||
toastService.error(`删除失败: ${result.error}`);
|
||||
console.error(`删除失败: ${result.error}`);
|
||||
|
||||
// 如果被删除的分组当前是展开状态,从展开列表中移除
|
||||
setExpandedGroups(prev => prev.filter(id => id !== groupId));
|
||||
|
||||
// 显示成功消息
|
||||
toastService.success('删除成功')
|
||||
} else {
|
||||
toastService.error(`删除失败: ${result.error}`);
|
||||
console.error(`删除失败: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error);
|
||||
toastService.error('删除分组失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error);
|
||||
toastService.error('删除分组失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 🆕 批量启用/禁用
|
||||
const handleBatchEnable = async (enable: boolean) => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要操作的分组');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await batchUpdateRuleGroupStatus(selectedIds, enable, frontendJWT);
|
||||
if (result.success) {
|
||||
toastService.success(`成功${enable ? '启用' : '禁用'} ${result.updated_count} 个分组`);
|
||||
// 刷新页面以重新加载数据
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(`批量操作失败:${result.failed_ids.length} 个分组操作失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
toastService.error('批量操作失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要删除的分组');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认批量删除",
|
||||
message: `确定要删除选中的 ${selectedIds.length} 个分组吗?此操作不可恢复。`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const result = await batchDeleteRuleGroups(selectedIds, frontendJWT);
|
||||
toastService.success(`成功删除 ${result.deleted_count} 个分组`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个分组删除失败`);
|
||||
}
|
||||
// 刷新页面以重新加载数据
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
toastService.error('批量删除失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 🆕 处理全选/取消全选
|
||||
const handleSelectAll = () => {
|
||||
if (selectedIds.length === groups.length) {
|
||||
setSelectedIds([]);
|
||||
} else {
|
||||
setSelectedIds(groups.map(g => g.id));
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 处理单选
|
||||
const handleSelectRow = (id: string) => {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(selectedId => selectedId !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
// 处理搜索名称
|
||||
@@ -450,6 +565,28 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
// 🆕 复选框列
|
||||
...(hasEditPermission ? [{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.length > 0 && selectedIds.length === groups.length}
|
||||
onChange={handleSelectAll}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
),
|
||||
key: "selection",
|
||||
width: "50px",
|
||||
render: (_: unknown, record: RuleGroup) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(record.id)}
|
||||
onChange={() => handleSelectRow(record.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
)
|
||||
}] : []),
|
||||
{
|
||||
title: "分组名称",
|
||||
key: "name",
|
||||
@@ -579,6 +716,34 @@ export default function RuleGroupsIndex() {
|
||||
>
|
||||
收起全部
|
||||
</Button>
|
||||
{hasEditPermission && selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-checkbox-circle-line"
|
||||
onClick={() => handleBatchEnable(true)}
|
||||
className="mr-2"
|
||||
>
|
||||
批量启用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-close-circle-line"
|
||||
onClick={() => handleBatchEnable(false)}
|
||||
className="mr-2"
|
||||
>
|
||||
批量禁用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="mr-2"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{hasEditPermission && (
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -100,12 +100,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// console.log("获取到的ID参数:", id);
|
||||
|
||||
// 获取一级分组列表 (用于选择父级分组)
|
||||
const parentGroupsResponse = await getRuleGroups(frontendJWT);
|
||||
// 🆕 使用增强的 getRuleGroups API,仅获取一级分组且已启用的分组
|
||||
const parentGroupsResponse = await getRuleGroups({
|
||||
pid: null, // 仅获取一级分组(pid为null表示顶级分组)
|
||||
is_enabled: true, // 仅获取已启用的分组
|
||||
pageSize: 100, // 获取足够多的分组
|
||||
token: frontendJWT
|
||||
});
|
||||
if (parentGroupsResponse.error) {
|
||||
console.error("获取父分组列表失败:", parentGroupsResponse.error);
|
||||
throw new Error(parentGroupsResponse.error);
|
||||
}
|
||||
|
||||
|
||||
const parentGroups: ParentGroup[] = parentGroupsResponse.data ? parentGroupsResponse.data.map(group => ({
|
||||
id: group.id,
|
||||
name: group.name
|
||||
@@ -194,7 +200,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
code: code.trim(),
|
||||
description: description?.trim() || "",
|
||||
is_enabled: status === "active",
|
||||
pid: parentId || undefined // 🆕 NULL/undefined 表示顶级分组
|
||||
pid: parentId || null // 🆕 NULL 表示顶级分组
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -274,10 +280,14 @@ export default function RuleGroupNew() {
|
||||
parentId?: string;
|
||||
general?: string;
|
||||
}>({});
|
||||
|
||||
|
||||
// 🆕 编码唯一性验证状态
|
||||
const [codeValidating, setCodeValidating] = useState(false);
|
||||
const [codeValidationTimer, setCodeValidationTimer] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 表单引用
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
|
||||
// 字段是否被触摸过(用于确定何时显示错误)
|
||||
const [touchedFields, setTouchedFields] = useState<{
|
||||
name: boolean;
|
||||
@@ -310,6 +320,44 @@ export default function RuleGroupNew() {
|
||||
}
|
||||
}, [group]);
|
||||
|
||||
// 🆕 异步验证编码唯一性
|
||||
const validateCodeUnique = async (code: string): Promise<string> => {
|
||||
if (!code || code.trim() === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
setCodeValidating(true);
|
||||
const response = await getRuleGroups({
|
||||
code: code.trim(),
|
||||
pageSize: 10,
|
||||
token: data.frontendJWT
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error("验证编码唯一性失败:", response.error);
|
||||
return ""; // 验证失败时不显示错误,避免干扰用户
|
||||
}
|
||||
|
||||
if (response.data && response.data.length > 0) {
|
||||
// 在编辑模式下,排除当前分组自身
|
||||
const isDuplicate = response.data.some(g =>
|
||||
g.id !== group?.id && g.code === code.trim()
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return "该编码已被使用,请使用其他编码";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
} catch (error) {
|
||||
console.error("验证编码唯一性出错:", error);
|
||||
return "";
|
||||
} finally {
|
||||
setCodeValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证表单字段
|
||||
const validateField = (field: string, value: string) => {
|
||||
switch (field) {
|
||||
@@ -323,8 +371,8 @@ export default function RuleGroupNew() {
|
||||
}
|
||||
return "";
|
||||
case 'parentId':
|
||||
return formValues.groupType === "secondary" && value.trim() === ""
|
||||
? "请选择上级分组"
|
||||
return formValues.groupType === "secondary" && value.trim() === ""
|
||||
? "请选择上级分组"
|
||||
: "";
|
||||
default:
|
||||
return "";
|
||||
@@ -334,12 +382,12 @@ export default function RuleGroupNew() {
|
||||
// 处理字段改变
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
|
||||
// 标记字段为已触摸
|
||||
if (['name', 'code', 'parentId'].includes(name)) {
|
||||
setTouchedFields(prev => ({
|
||||
@@ -347,13 +395,34 @@ export default function RuleGroupNew() {
|
||||
[name]: true
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// 实时验证
|
||||
const error = validateField(name, value);
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
[name]: error
|
||||
}));
|
||||
|
||||
// 🆕 编码字段特殊处理:异步验证唯一性(防抖处理)
|
||||
if (name === 'code' && !error) {
|
||||
// 清除之前的定时器
|
||||
if (codeValidationTimer) {
|
||||
clearTimeout(codeValidationTimer);
|
||||
}
|
||||
|
||||
// 设置新的定时器,500ms后执行验证
|
||||
const timer = setTimeout(async () => {
|
||||
const uniqueError = await validateCodeUnique(value);
|
||||
if (uniqueError) {
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
code: uniqueError
|
||||
}));
|
||||
}
|
||||
}, 500);
|
||||
|
||||
setCodeValidationTimer(timer);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理分组类型更改
|
||||
@@ -551,6 +620,7 @@ export default function RuleGroupNew() {
|
||||
<div className="form-group">
|
||||
<label htmlFor="code" className="form-label">
|
||||
分组编码 <span className="required-mark">*</span>
|
||||
{codeValidating && <span className="ml-2 text-sm text-gray-500">验证中...</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
+155
-10
@@ -15,13 +15,15 @@ import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterP
|
||||
import { Pagination } from '~/components/ui/Pagination';
|
||||
import { messageService } from '~/components/ui/MessageModal';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import {
|
||||
getRulesList,
|
||||
deleteRule,
|
||||
getRuleTypes,
|
||||
import {
|
||||
getRulesList,
|
||||
deleteRule,
|
||||
getRuleTypes,
|
||||
getRuleGroupsByType,
|
||||
batchUpdateRuleStatus,
|
||||
batchDeleteRules,
|
||||
type RuleType as ApiRuleType,
|
||||
type RuleGroup
|
||||
type RuleGroup
|
||||
} from '~/api/evaluation_points/rules';
|
||||
import type { UserRole } from '~/root';
|
||||
|
||||
@@ -209,6 +211,9 @@ export default function RulesIndex() {
|
||||
// 添加一个状态来跟踪是否执行了删除操作
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 批量选择状态
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
// 使用 ref 跟踪是否正在加载数据,避免重复加载
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
@@ -541,6 +546,92 @@ export default function RulesIndex() {
|
||||
navigate(`/rules/new?id=${rule.id}&mode=copy`);
|
||||
};
|
||||
|
||||
// 批量选择处理
|
||||
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedIds(filteredRules.map(rule => rule.id));
|
||||
} else {
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRow = (id: string) => {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
// 批量启用/禁用
|
||||
const handleBatchEnable = async (isEnabled: boolean) => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要操作的评查点');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await batchUpdateRuleStatus(selectedIds, isEnabled, loaderData.frontendJWT);
|
||||
|
||||
if (result.success) {
|
||||
toastService.success(`成功${isEnabled ? '启用' : '禁用'} ${result.updated_count} 个评查点`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个评查点操作失败`);
|
||||
}
|
||||
// 清空选择
|
||||
setSelectedIds([]);
|
||||
// 重新加载数据
|
||||
fetchData();
|
||||
} else {
|
||||
toastService.error('批量操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
toastService.error('批量操作失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要删除的评查点');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认批量删除",
|
||||
message: `确定要删除选中的 ${selectedIds.length} 个评查点吗?此操作不可恢复。`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await batchDeleteRules(selectedIds, loaderData.frontendJWT);
|
||||
|
||||
if (result.success) {
|
||||
toastService.success(`成功删除 ${result.deleted_count} 个评查点`);
|
||||
if (result.failed_ids.length > 0) {
|
||||
toastService.warning(`有 ${result.failed_ids.length} 个评查点删除失败`);
|
||||
}
|
||||
// 清空选择
|
||||
setSelectedIds([]);
|
||||
// 重新加载数据
|
||||
fetchData();
|
||||
} else {
|
||||
toastService.error('批量删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
toastService.error('批量删除失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('page', page.toString());
|
||||
@@ -573,6 +664,28 @@ export default function RulesIndex() {
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
// 添加复选框列(仅开发者可见)
|
||||
...(isDeveloper ? [{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.length > 0 && selectedIds.length === filteredRules.length && filteredRules.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
disabled={filteredRules.length === 0}
|
||||
/>
|
||||
),
|
||||
key: "selection",
|
||||
align: "center" as const,
|
||||
width: "50px",
|
||||
render: (_: unknown, record: Rule) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(record.id)}
|
||||
onChange={() => handleSelectRow(record.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)
|
||||
}] : []),
|
||||
{
|
||||
title: "评查点编码",
|
||||
dataIndex: "code" as keyof Rule,
|
||||
@@ -691,11 +804,43 @@ export default function RulesIndex() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDeveloper && (
|
||||
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
|
||||
新增评查点
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 批量操作按钮(仅在有选择时显示) */}
|
||||
{isDeveloper && selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-check-line"
|
||||
onClick={() => handleBatchEnable(true)}
|
||||
className="btn-batch-enable"
|
||||
>
|
||||
批量启用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-close-line"
|
||||
onClick={() => handleBatchEnable(false)}
|
||||
className="btn-batch-disable"
|
||||
>
|
||||
批量禁用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="btn-batch-delete"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* 新增按钮 */}
|
||||
{isDeveloper && (
|
||||
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
|
||||
新增评查点
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
|
||||
+66
-62
@@ -48,10 +48,17 @@ import { EVALUATION_OPTIONS } from "~/models/evaluation_points";
|
||||
import type { EvaluationPointGroup } from "~/models/evaluation_point_groups";
|
||||
// 导入RuleContext上下文
|
||||
import { RuleContext } from "~/contexts/RuleContext";
|
||||
import { postgrestGet, postgrestPost, postgrestPut } from "~/api/postgrest-client";
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import type { UserRole } from '~/root';
|
||||
import { getPromptTemplateOptions } from '~/api/prompts/prompts';
|
||||
import {
|
||||
createEvaluationPoint,
|
||||
updateEvaluationPoint,
|
||||
getEvaluationPoint,
|
||||
type EvaluationPointData
|
||||
} from '~/api/evaluation_points/rules';
|
||||
import { getRulesList } from '~/api/evaluation_points/rules';
|
||||
import { postgrestGet } from '~/api/postgrest-client';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -276,69 +283,62 @@ export default function RuleNew() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// console.log(`获取评查点数据,ID: ${id}, 复制模式: ${isCopy}`);
|
||||
// 使用 postgrestGet 替代直接调用 fetch
|
||||
const postgrestParams = {
|
||||
filter: {
|
||||
'id': `eq.${id}`
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const response = await postgrestGet('evaluation_points', postgrestParams);
|
||||
// 使用新的 getEvaluationPoint API 获取数据
|
||||
const response = await getEvaluationPoint(String(id), frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
// API返回错误
|
||||
toastService.error(`获取评查点数据失败: ${response.error}`);
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
// 使用extractApiData从响应中提取数据
|
||||
const evaluationPoints = extractApiData<EvaluationPoint[]>(response.data);
|
||||
try {
|
||||
// 使用JSON序列化和反序列化来进行深拷贝,避免浏览器差异
|
||||
const originalData = response.data;
|
||||
const jsonString = JSON.stringify(originalData);
|
||||
const data = JSON.parse(jsonString) as EvaluationPointData;
|
||||
|
||||
if (evaluationPoints && Array.isArray(evaluationPoints) && evaluationPoints.length > 0) {
|
||||
try {
|
||||
// 使用JSON序列化和反序列化来进行深拷贝,避免浏览器差异
|
||||
const originalData = evaluationPoints[0];
|
||||
const jsonString = JSON.stringify(originalData);
|
||||
const data = JSON.parse(jsonString);
|
||||
|
||||
// 🔄 复制模式:删除不应该复制的字段
|
||||
if (isCopy) {
|
||||
delete data.id;
|
||||
delete data.created_at;
|
||||
delete data.updated_at;
|
||||
delete data.usage_count;
|
||||
|
||||
// console.log('📋 复制模式:已清除不应复制的字段(id, created_at, updated_at, usage_count)');
|
||||
// 🔄 复制模式:删除不应该复制的字段
|
||||
if (isCopy) {
|
||||
delete data.id;
|
||||
delete data.created_at;
|
||||
delete data.updated_at;
|
||||
// usage_count 不在 EvaluationPointData 接口中,但可能存在于响应数据中
|
||||
if ('usage_count' in data) {
|
||||
delete (data as Record<string, unknown>).usage_count;
|
||||
}
|
||||
|
||||
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
|
||||
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
|
||||
if (data.code) {
|
||||
const lastDoubleHyphenIndex = data.code.lastIndexOf('--');
|
||||
if (lastDoubleHyphenIndex !== -1) {
|
||||
data.code = data.code.substring(0, lastDoubleHyphenIndex);
|
||||
// console.log('🔑 已清洗评查点编码:', data.code);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单数据
|
||||
setFormData(data);
|
||||
|
||||
// 初始化extractionFields
|
||||
const extractedFields = extractFieldsFromFormData(data);
|
||||
setExtractionFields(extractedFields);
|
||||
|
||||
// 设置实例键
|
||||
setInstanceKey(isCopy ? `copy_${id}_${Date.now()}` : `edit_${id}_${Date.now()}`);
|
||||
} catch (jsonError) {
|
||||
console.error('JSON处理错误:', jsonError);
|
||||
toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
// console.log('📋 复制模式:已清除不应复制的字段(id, created_at, updated_at, usage_count)');
|
||||
}
|
||||
} else {
|
||||
console.error('获取数据失败: 返回数据为空');
|
||||
toastService.error('获取数据失败: 返回数据为空');
|
||||
|
||||
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
|
||||
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
|
||||
if (data.code) {
|
||||
const lastDoubleHyphenIndex = data.code.lastIndexOf('--');
|
||||
if (lastDoubleHyphenIndex !== -1) {
|
||||
data.code = data.code.substring(0, lastDoubleHyphenIndex);
|
||||
// console.log('🔑 已清洗评查点编码:', data.code);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单数据(EvaluationPointData 兼容 EvaluationPoint)
|
||||
setFormData(data as EvaluationPoint);
|
||||
|
||||
// 初始化extractionFields
|
||||
const extractedFields = extractFieldsFromFormData(data as EvaluationPoint);
|
||||
setExtractionFields(extractedFields);
|
||||
|
||||
// 设置实例键
|
||||
setInstanceKey(isCopy ? `copy_${id}_${Date.now()}` : `edit_${id}_${Date.now()}`);
|
||||
} catch (jsonError) {
|
||||
console.error('JSON处理错误:', jsonError);
|
||||
toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`响应状态: ${response.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评查点数据失败:', error);
|
||||
@@ -801,27 +801,29 @@ export default function RuleNew() {
|
||||
|
||||
let response;
|
||||
if (isEditMode) {
|
||||
response = await postgrestPut('evaluation_points', finalData, {id: formData.id!}, frontendJWT);
|
||||
// 使用新的 updateEvaluationPoint API
|
||||
response = await updateEvaluationPoint(String(formData.id!), finalData, frontendJWT);
|
||||
// console.log("最终提交的数据", finalData)
|
||||
} else {
|
||||
response = await postgrestPost('evaluation_points', finalData, frontendJWT);
|
||||
// 使用新的 createEvaluationPoint API
|
||||
response = await createEvaluationPoint(finalData as Omit<EvaluationPointData, 'id' | 'created_at' | 'updated_at'>, frontendJWT);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (response.error.includes('evaluation_points_code_key')) {
|
||||
toastService.error('在基本信息中:评查点编码已存在,请修改后保存。');
|
||||
} else {
|
||||
} else {
|
||||
toastService.error(`系统繁忙: ${response.error}`);
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
} else if (response.data) {
|
||||
// 获取新创建或更新的评查点ID
|
||||
const savedPointId = response.data[0]?.id;
|
||||
|
||||
const savedPointId = response.data.id;
|
||||
|
||||
if (savedPointId) {
|
||||
// 显示成功消息
|
||||
toastService.success(`评查点${isEditMode ? '更新' : '创建'}成功!`);
|
||||
|
||||
|
||||
// 保存成功后跳转到编辑页面并重新加载数据
|
||||
navigate(`/rules/new?id=${savedPointId}`, { replace: true });
|
||||
// 重新获取评查点数据
|
||||
@@ -1051,6 +1053,8 @@ export default function RuleNew() {
|
||||
initialData={formData}
|
||||
evaluationPointGroups={evaluationPointGroups}
|
||||
riskOptions={EVALUATION_OPTIONS.riskLevelOptions}
|
||||
frontendJWT={frontendJWT}
|
||||
evaluationPointId={formData.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* RBAC Mock数据 - 共享存储
|
||||
* 所有Remix API路由共享这个数据源
|
||||
*/
|
||||
|
||||
// ==================== 角色数据(与数据库实际数据一致)====================
|
||||
|
||||
export const mockRoles = [
|
||||
{
|
||||
id: 1,
|
||||
role_key: 'admin',
|
||||
role_name: '市级管理员',
|
||||
description: '负责本地区的所有业务管理,不包括系统设置和角色权限管理',
|
||||
data_scope: 'DEPT',
|
||||
is_system: false,
|
||||
priority: 0,
|
||||
created_at: '2025-07-18T10:35:39+08:00',
|
||||
updated_at: '2025-07-18T10:35:39+08:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
role_key: 'common',
|
||||
role_name: '普通员工',
|
||||
description: '仅能操作自己的数据',
|
||||
data_scope: 'SELF',
|
||||
is_system: false,
|
||||
priority: 0,
|
||||
created_at: '2025-07-18T10:35:39+08:00',
|
||||
updated_at: '2025-07-18T10:35:39+08:00'
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
role_key: 'provincial_admin',
|
||||
role_name: '省级管理员',
|
||||
description: '拥有全部权限,可以管理所有地区的评查点规则、提示词、动态按钮、评查组',
|
||||
data_scope: 'ALL',
|
||||
is_system: true,
|
||||
priority: 1,
|
||||
created_at: '2025-11-19T17:25:45+08:00',
|
||||
updated_at: '2025-11-19T17:25:45+08:00'
|
||||
}
|
||||
];
|
||||
|
||||
// ==================== 用户数据 ====================
|
||||
|
||||
export const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
user_id: 1,
|
||||
username: 'admin',
|
||||
nick_name: '系统管理员',
|
||||
phone_number: '13800138000',
|
||||
email: 'admin@example.com',
|
||||
ou_name: '广东省烟草专卖局',
|
||||
area: '广东省',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user_id: 2,
|
||||
username: 'zhangsan',
|
||||
nick_name: '张三',
|
||||
phone_number: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
ou_name: '梅州市烟草专卖局',
|
||||
area: '梅州',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user_id: 3,
|
||||
username: 'lisi',
|
||||
nick_name: '李四',
|
||||
phone_number: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
ou_name: '云浮市烟草专卖局',
|
||||
area: '云浮',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
user_id: 4,
|
||||
username: 'wangwu',
|
||||
nick_name: '王五',
|
||||
phone_number: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
ou_name: '揭阳市烟草专卖局',
|
||||
area: '揭阳',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
user_id: 5,
|
||||
username: 'zhaoliu',
|
||||
nick_name: '赵六',
|
||||
phone_number: '13800138004',
|
||||
email: 'zhaoliu@example.com',
|
||||
ou_name: '潮州市烟草专卖局',
|
||||
area: '潮州',
|
||||
status: 1,
|
||||
is_leader: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
user_id: 6,
|
||||
username: 'sunqi',
|
||||
nick_name: '孙七',
|
||||
phone_number: '13800138005',
|
||||
email: 'sunqi@example.com',
|
||||
ou_name: '汕头市烟草专卖局',
|
||||
area: '汕头',
|
||||
status: 1,
|
||||
is_leader: true
|
||||
}
|
||||
];
|
||||
|
||||
// ==================== 用户-角色关联数据 ====================
|
||||
|
||||
export const mockUserRoles: Array<{ user_id: number; role_id: number; assigned_at: string }> = [
|
||||
{ user_id: 1, role_id: 52, assigned_at: '2025-01-20T10:00:00' }, // admin - provincial_admin (id=52)
|
||||
{ user_id: 2, role_id: 1, assigned_at: '2025-01-21T10:00:00' }, // zhangsan - 市级管理员 (id=1)
|
||||
{ user_id: 3, role_id: 1, assigned_at: '2025-01-21T11:00:00' }, // lisi - 市级管理员 (id=1)
|
||||
{ user_id: 4, role_id: 2, assigned_at: '2025-01-21T12:00:00' }, // wangwu - 普通员工 (id=2)
|
||||
];
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
/**
|
||||
* 获取角色的用户列表
|
||||
*/
|
||||
export function getRoleUsers(roleId: number) {
|
||||
const userIds = mockUserRoles
|
||||
.filter(ur => ur.role_id === roleId)
|
||||
.map(ur => ur.user_id);
|
||||
|
||||
return mockUsers.filter(u => userIds.includes(u.id || u.user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
*/
|
||||
export function assignUserRole(userId: number, roleId: number) {
|
||||
// 检查是否已存在
|
||||
const exists = mockUserRoles.some(
|
||||
ur => ur.user_id === userId && ur.role_id === roleId
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
mockUserRoles.push({
|
||||
user_id: userId,
|
||||
role_id: roleId,
|
||||
assigned_at: new Date().toISOString()
|
||||
});
|
||||
console.log('✅ [Mock Data] 用户角色分配成功:', { userId, roleId });
|
||||
console.log('📊 [Mock Data] 当前用户-角色关联:', mockUserRoles);
|
||||
} else {
|
||||
console.log('⚠️ [Mock Data] 用户角色关联已存在:', { userId, roleId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户角色
|
||||
*/
|
||||
export function removeUserRole(userId: number, roleId: number) {
|
||||
const index = mockUserRoles.findIndex(
|
||||
ur => ur.user_id === userId && ur.role_id === roleId
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
mockUserRoles.splice(index, 1);
|
||||
console.log('✅ [Mock Data] 用户角色移除成功:', { userId, roleId });
|
||||
console.log('📊 [Mock Data] 当前用户-角色关联:', mockUserRoles);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('⚠️ [Mock Data] 用户角色关联不存在:', { userId, roleId });
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新角色
|
||||
*/
|
||||
export function addRole(roleData: any) {
|
||||
const newRole = {
|
||||
id: Math.max(...mockRoles.map(r => r.id), 0) + 1,
|
||||
...roleData,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
mockRoles.push(newRole);
|
||||
console.log('✅ [Mock Data] 角色创建成功:', newRole);
|
||||
return newRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
export function updateRole(roleId: number, updates: any) {
|
||||
const role = mockRoles.find(r => r.id === roleId);
|
||||
if (role) {
|
||||
Object.assign(role, updates, {
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
console.log('✅ [Mock Data] 角色更新成功:', role);
|
||||
return role;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
export function deleteRole(roleId: number) {
|
||||
const index = mockRoles.findIndex(r => r.id === roleId);
|
||||
if (index > -1) {
|
||||
const deleted = mockRoles.splice(index, 1)[0];
|
||||
console.log('✅ [Mock Data] 角色删除成功:', deleted);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user