fix: 1. 继续对齐交叉评查的接口,完善创建交叉评查的逻辑 和 相关组件的渲染布局。

2. 文档的基本信息修改改用接口。      3. 重新完善角色权限管理的页面逻辑。     4.将评查点列表中的返回逻辑改用浏览器的记忆返回。
This commit is contained in:
2025-12-12 12:00:36 +08:00
parent a5c49a5c95
commit d4000cd292
25 changed files with 4750 additions and 28293 deletions
+139 -8
View File
@@ -12,6 +12,9 @@ type MultiCascaderProps = {
value?: string[];
onChange?: (value: string[]) => void;
placeholder?: string;
maxHeight?: number; // 下拉框最大高度,默认300px
searchable?: boolean; // 是否显示搜索框,默认false
searchPlaceholder?: string; // 搜索框占位符
};
// 获取所有叶子节点的值
@@ -42,17 +45,87 @@ const isSomeChildrenChecked = (option: Option, selected: string[]): boolean => {
return leafValues.some(value => selected.includes(value));
};
const MultiCascader: React.FC<MultiCascaderProps> = ({
options,
defaultValue = [],
// 递归过滤选项,保留匹配项及其父级
const filterOptions = (options: Option[], keyword: string): Option[] => {
if (!keyword.trim()) return options;
const lowerKeyword = keyword.toLowerCase();
return options.reduce<Option[]>((acc, option) => {
// 检查当前节点是否匹配
const isCurrentMatch = option.label.toLowerCase().includes(lowerKeyword);
// 递归检查子节点
const filteredChildren = option.children ? filterOptions(option.children, keyword) : [];
// 如果当前节点匹配,或者有匹配的子节点,则保留
if (isCurrentMatch || filteredChildren.length > 0) {
acc.push({
...option,
children: isCurrentMatch
? option.children // 如果父节点匹配,保留所有子节点
: filteredChildren.length > 0
? filteredChildren // 如果只是子节点匹配,只保留匹配的子节点
: option.children
});
}
return acc;
}, []);
};
// 获取过滤后需要展开的所有父节点
const getExpandedKeysForFilter = (options: Option[], keyword: string): Set<string> => {
const keys = new Set<string>();
if (!keyword.trim()) return keys;
const lowerKeyword = keyword.toLowerCase();
const traverse = (opts: Option[], parentKeys: string[]) => {
for (const opt of opts) {
const isMatch = opt.label.toLowerCase().includes(lowerKeyword);
const hasChildren = opt.children && opt.children.length > 0;
if (hasChildren) {
// 递归检查子节点
const childMatches = traverse(opt.children!, [...parentKeys, opt.value]);
// 如果子节点有匹配,展开当前节点
if (childMatches) {
keys.add(opt.value);
parentKeys.forEach(k => keys.add(k));
}
}
// 如果当前节点匹配且有父节点,展开父节点
if (isMatch && parentKeys.length > 0) {
parentKeys.forEach(k => keys.add(k));
return true;
}
}
return false;
};
traverse(options, []);
return keys;
};
const MultiCascader: React.FC<MultiCascaderProps> = ({
options,
defaultValue = [],
value,
onChange,
placeholder = '请选择'
onChange,
placeholder = '请选择',
maxHeight = 300,
searchable = false,
searchPlaceholder = '搜索...'
}) => {
const [visible, setVisible] = useState(false);
const [selected, setSelected] = useState<string[]>(value ?? defaultValue);
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
const [searchKeyword, setSearchKeyword] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
// 当外部 value 变化时,同步内部状态
useEffect(() => {
@@ -71,6 +144,27 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// 搜索关键词变化时自动展开匹配项
useEffect(() => {
if (searchKeyword.trim()) {
const keysToExpand = getExpandedKeysForFilter(options, searchKeyword);
setExpandedKeys(keysToExpand);
}
}, [searchKeyword, options]);
// 下拉框打开时聚焦搜索框
useEffect(() => {
if (visible && searchable && searchInputRef.current) {
setTimeout(() => searchInputRef.current?.focus(), 100);
}
if (!visible) {
setSearchKeyword(''); // 关闭时清空搜索
}
}, [visible, searchable]);
// 过滤后的选项
const filteredOptions = searchable ? filterOptions(options, searchKeyword) : options;
const handleItemCheck = (option: Option, checked: boolean) => {
const leafValues = getAllLeafValues(option);
let newSelected: string[];
@@ -175,9 +269,46 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
</span>
</div>
{visible && (
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 w-full max-h-100 overflow-y-auto">
<div className="p-2">
{options.map(option => renderOption(option))}
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 w-full">
{/* 搜索框 */}
{searchable && (
<div className="p-2 border-b border-gray-200">
<div className="relative">
<i className="ri-search-line absolute left-2 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
ref={searchInputRef}
type="text"
className="w-full pl-8 pr-3 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary,#00684a)] focus:border-[var(--color-primary,#00684a)]"
placeholder={searchPlaceholder}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
{searchKeyword && (
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
onClick={(e) => {
e.stopPropagation();
setSearchKeyword('');
}}
>
<i className="ri-close-line"></i>
</button>
)}
</div>
</div>
)}
{/* 选项列表 */}
<div className="p-2 overflow-y-auto" style={{ maxHeight: `${maxHeight}px` }}>
{filteredOptions.length > 0 ? (
filteredOptions.map(option => renderOption(option))
) : (
<div className="text-center text-gray-400 py-4 text-sm">
<i className="ri-search-line text-lg block mb-1"></i>
</div>
)}
</div>
</div>
)}