Merge branch 'Wren' into shiy-login

This commit is contained in:
2025-11-25 18:24:28 +08:00
30 changed files with 10523 additions and 747 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+83 -5
View File
@@ -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">
+35 -9
View File
@@ -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}
+1 -1
View File
@@ -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';
+86
View File
@@ -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
}
});
}
+99
View File
@@ -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 });
}
+35 -26
View File
@@ -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);
}
}
});
};
// 处理编辑文档类型
+13 -11
View File
@@ -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([]);
}
+31 -22
View File
@@ -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);
}
}
});
};
// 处理编辑入口模块
+15 -7
View File
@@ -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 状态变化
+204 -39
View File
@@ -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"
+80 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+226
View File
@@ -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;
}