1. 登录返回总公司,分公司,部门信息。
2. 修改角色权限管理的分配用户的数据渲染和接口。 3. 交叉评查任务的创建的组织架构组件的重构。
This commit is contained in:
+360
-202
@@ -1,254 +1,407 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
// 组织路径信息(懒加载接口返回)
|
||||
export interface OrganizationPath {
|
||||
tenant_name: string;
|
||||
dep_name: string;
|
||||
dep_short_name: string;
|
||||
ou_name: string;
|
||||
}
|
||||
|
||||
// 导入类型
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
nick_name: string;
|
||||
area: string;
|
||||
ou_id: string;
|
||||
ou_name: string;
|
||||
is_leader: boolean;
|
||||
status: number;
|
||||
// 以下字段在搜索接口中有值,在懒加载接口中为 null
|
||||
tenant_name: string | null;
|
||||
dep_name: string | null;
|
||||
dep_short_name: string | null;
|
||||
email?: string;
|
||||
phone_number?: string;
|
||||
// 懒加载接口返回的组织路径信息
|
||||
organization_path?: OrganizationPath | null;
|
||||
}
|
||||
|
||||
export interface TreeNodeItem {
|
||||
label: string;
|
||||
children?: Option[];
|
||||
};
|
||||
value: string;
|
||||
isUser: boolean;
|
||||
hasChildren: boolean;
|
||||
userInfo?: UserInfo;
|
||||
children?: TreeNodeItem[];
|
||||
// 懒加载状态
|
||||
isLoading?: boolean;
|
||||
isLoaded?: boolean;
|
||||
}
|
||||
|
||||
type MultiCascaderProps = {
|
||||
options: Option[];
|
||||
defaultValue?: string[];
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
options: TreeNodeItem[];
|
||||
// 已选择的用户ID列表(用于判断按钮状态)
|
||||
selectedUsers?: string[];
|
||||
placeholder?: string;
|
||||
maxHeight?: number; // 下拉框最大高度,默认300px
|
||||
searchable?: boolean; // 是否显示搜索框,默认false
|
||||
searchPlaceholder?: string; // 搜索框占位符
|
||||
};
|
||||
|
||||
// 获取所有叶子节点的值
|
||||
const getAllLeafValues = (option: Option): string[] => {
|
||||
if (!option.children || option.children.length === 0) {
|
||||
return [option.value];
|
||||
}
|
||||
return option.children.flatMap(getAllLeafValues);
|
||||
};
|
||||
|
||||
// 检查节点是否所有子节点都被选中
|
||||
const isAllChildrenChecked = (option: Option, selected: string[]): boolean => {
|
||||
if (!option.children || option.children.length === 0) {
|
||||
return selected.includes(option.value);
|
||||
}
|
||||
// 对于有子节点的节点,检查其所有叶子节点是否都被选中
|
||||
const leafValues = getAllLeafValues(option);
|
||||
return leafValues.every(value => selected.includes(value));
|
||||
};
|
||||
|
||||
// 检查节点是否有部分子节点被选中
|
||||
const isSomeChildrenChecked = (option: Option, selected: string[]): boolean => {
|
||||
if (!option.children || option.children.length === 0) {
|
||||
return selected.includes(option.value);
|
||||
}
|
||||
// 对于有子节点的节点,检查其叶子节点是否有部分被选中
|
||||
const leafValues = getAllLeafValues(option);
|
||||
return leafValues.some(value => selected.includes(value));
|
||||
};
|
||||
|
||||
// 递归过滤选项,保留匹配项及其父级
|
||||
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;
|
||||
maxHeight?: number;
|
||||
searchable?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
// 懒加载回调
|
||||
onLoadChildren?: (node: TreeNodeItem) => Promise<TreeNodeItem[]>;
|
||||
// 搜索用户回调
|
||||
onSearchUsers?: (keyword: string) => Promise<TreeNodeItem[]>;
|
||||
// 树数据变化回调(懒加载后同步更新父组件的树数据)
|
||||
onTreeDataChange?: (treeData: TreeNodeItem[]) => void;
|
||||
// 用户从树中添加时的回调
|
||||
onAddUser?: (userNode: TreeNodeItem) => void;
|
||||
// 用户从搜索结果添加时的回调
|
||||
onSearchUserAdded?: (userNode: TreeNodeItem) => void;
|
||||
};
|
||||
|
||||
const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
options,
|
||||
defaultValue = [],
|
||||
value,
|
||||
onChange,
|
||||
selectedUsers = [],
|
||||
placeholder = '请选择',
|
||||
maxHeight = 300,
|
||||
searchable = false,
|
||||
searchPlaceholder = '搜索...'
|
||||
searchPlaceholder = '搜索...',
|
||||
onLoadChildren,
|
||||
onSearchUsers,
|
||||
onTreeDataChange,
|
||||
onAddUser,
|
||||
onSearchUserAdded
|
||||
}) => {
|
||||
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 [searchResults, setSearchResults] = useState<TreeNodeItem[]>([]);
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [localOptions, setLocalOptions] = useState<TreeNodeItem[]>(options);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||||
const prevOptionsRef = useRef<TreeNodeItem[]>(options);
|
||||
|
||||
// 当外部 value 变化时,同步内部状态
|
||||
// 同步外部 options 变化
|
||||
useEffect(() => {
|
||||
if (value !== undefined) {
|
||||
setSelected(value);
|
||||
}
|
||||
}, [value]);
|
||||
setLocalOptions(options);
|
||||
}, [options]);
|
||||
|
||||
// 同步树数据变化到父组件(懒加载后触发)
|
||||
useEffect(() => {
|
||||
// 只有当 localOptions 真正变化时才调用回调,避免无限循环
|
||||
if (onTreeDataChange && localOptions !== prevOptionsRef.current) {
|
||||
prevOptionsRef.current = localOptions;
|
||||
onTreeDataChange(localOptions);
|
||||
}
|
||||
}, [localOptions, onTreeDataChange]);
|
||||
|
||||
// 点击外部关闭
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setVisible(false);
|
||||
setSearchKeyword('');
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
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(''); // 关闭时清空搜索
|
||||
setSearchKeyword('');
|
||||
setSearchResults([]);
|
||||
}
|
||||
}, [visible, searchable]);
|
||||
|
||||
// 过滤后的选项
|
||||
const filteredOptions = searchable ? filterOptions(options, searchKeyword) : options;
|
||||
// 搜索用户
|
||||
const handleSearch = useCallback(async (keyword: string) => {
|
||||
setSearchKeyword(keyword);
|
||||
|
||||
const handleItemCheck = (option: Option, checked: boolean) => {
|
||||
const leafValues = getAllLeafValues(option);
|
||||
let newSelected: string[];
|
||||
|
||||
if (checked) {
|
||||
newSelected = Array.from(new Set([...selected, ...leafValues]));
|
||||
} else {
|
||||
newSelected = selected.filter(v => !leafValues.includes(v));
|
||||
if (!keyword.trim() || !onSearchUsers) {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelected(newSelected);
|
||||
onChange?.(newSelected);
|
||||
};
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (selected.length === 0) return placeholder;
|
||||
return `已选择 ${selected.length} 项`;
|
||||
};
|
||||
setSearchLoading(true);
|
||||
try {
|
||||
const results = await onSearchUsers(keyword);
|
||||
setSearchResults(results);
|
||||
} catch (error) {
|
||||
console.error('搜索用户失败:', error);
|
||||
setSearchResults([]);
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
}, [onSearchUsers]);
|
||||
|
||||
const toggleExpand = (key: string) => {
|
||||
setExpandedKeys(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(key)) {
|
||||
newSet.delete(key);
|
||||
} else {
|
||||
newSet.add(key);
|
||||
// 防抖搜索
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (searchKeyword && searchable && onSearchUsers) {
|
||||
handleSearch(searchKeyword);
|
||||
}
|
||||
return newSet;
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchKeyword, searchable, onSearchUsers, handleSearch]);
|
||||
|
||||
// 懒加载子节点
|
||||
const handleExpand = async (node: TreeNodeItem) => {
|
||||
// 如果已经加载过(有子组织或用户),则直接展开/折叠,不调用API
|
||||
if (node.isLoaded) {
|
||||
setExpandedKeys(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(node.value)) {
|
||||
newSet.delete(node.value); // 折叠
|
||||
} else {
|
||||
newSet.add(node.value); // 展开
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 正在加载中,直接展开已加载的部分
|
||||
if (loadingNodesRef.current.has(node.value)) {
|
||||
setExpandedKeys(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(node.value);
|
||||
return newSet;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记为加载中
|
||||
loadingNodesRef.current.add(node.value);
|
||||
setLocalOptions(prev => updateNodeLoading(prev, node.value, true));
|
||||
|
||||
try {
|
||||
if (onLoadChildren) {
|
||||
const children = await onLoadChildren(node);
|
||||
|
||||
// 更新本地选项
|
||||
setLocalOptions(prev => updateNodeChildren(prev, node.value, children, true));
|
||||
}
|
||||
|
||||
// 展开节点
|
||||
setExpandedKeys(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(node.value);
|
||||
return newSet;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载子节点失败:', error);
|
||||
} finally {
|
||||
loadingNodesRef.current.delete(node.value);
|
||||
setLocalOptions(prev => updateNodeLoading(prev, node.value, false));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新节点加载状态
|
||||
const updateNodeLoading = (nodes: TreeNodeItem[], value: string, isLoading: boolean): TreeNodeItem[] => {
|
||||
return nodes.map(node => {
|
||||
if (node.value === value) {
|
||||
return { ...node, isLoading };
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: updateNodeLoading(node.children, value, isLoading) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const renderOption = (option: Option, level: number = 0) => {
|
||||
const allChecked = isAllChildrenChecked(option, selected);
|
||||
const someChecked = isSomeChildrenChecked(option, selected);
|
||||
const hasChildren = option.children && option.children.length > 0;
|
||||
const isExpanded = expandedKeys.has(option.value);
|
||||
|
||||
// 更新节点的子节点
|
||||
const updateNodeChildren = (
|
||||
nodes: TreeNodeItem[],
|
||||
value: string,
|
||||
children: TreeNodeItem[],
|
||||
isLoaded: boolean
|
||||
): TreeNodeItem[] => {
|
||||
return nodes.map(node => {
|
||||
if (node.value === value) {
|
||||
return {
|
||||
...node,
|
||||
children,
|
||||
isLoaded,
|
||||
hasChildren: children.length > 0
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateNodeChildren(node.children, value, children, isLoaded)
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
// 检查用户是否已添加
|
||||
const isUserAdded = (value: string): boolean => {
|
||||
return selectedUsers.includes(value);
|
||||
};
|
||||
|
||||
// 从树中添加用户
|
||||
const handleAddUser = (userNode: TreeNodeItem) => {
|
||||
if (isUserAdded(userNode.value)) {
|
||||
return; // 已添加,不处理
|
||||
}
|
||||
onAddUser?.(userNode);
|
||||
};
|
||||
|
||||
// 从搜索结果添加用户
|
||||
const handleAddSearchUser = (userNode: TreeNodeItem) => {
|
||||
if (isUserAdded(userNode.value)) {
|
||||
return; // 已添加,不处理
|
||||
}
|
||||
onSearchUserAdded?.(userNode);
|
||||
};
|
||||
|
||||
// 渲染组织/用户节点
|
||||
const renderNode = (node: TreeNodeItem, level: number = 0, parentKey: string = ''): React.ReactNode => {
|
||||
const hasChildren = node.hasChildren;
|
||||
const isExpanded = expandedKeys.has(node.value);
|
||||
const isLoading = node.isLoading;
|
||||
const isLoaded = node.isLoaded;
|
||||
const isAdded = isUserAdded(node.value);
|
||||
|
||||
const nodeKey = parentKey ? `${parentKey}-${node.value}` : node.value;
|
||||
|
||||
return (
|
||||
<div key={option.value} className={level > 0 ? "pl-4" : ""}>
|
||||
<div className="flex items-center py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
style={{
|
||||
accentColor: '#00684a', // 固定为绿色(烟草企业绿)
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
checked={allChecked}
|
||||
ref={el => { if (el) el.indeterminate = !allChecked && someChecked; }}
|
||||
onChange={e => handleItemCheck(option, e.target.checked)}
|
||||
id={`cascader-${option.value}`}
|
||||
/>
|
||||
<label htmlFor={`cascader-${option.value}`} className="ml-2 text-sm flex-1 cursor-pointer">
|
||||
{option.label}
|
||||
</label>
|
||||
{hasChildren && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2"
|
||||
style={{ color: 'gray' }} // 展开图标固定为绿色
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpand(option.value);
|
||||
}}
|
||||
>
|
||||
<i className={isExpanded ? "ri-arrow-down-s-line" : "ri-arrow-right-s-line"}></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{hasChildren && isExpanded && (
|
||||
<div key={nodeKey} className={level > 0 ? "ml-4" : ""}>
|
||||
{/* 组织节点:点击整行可展开/收缩 */}
|
||||
{hasChildren ? (
|
||||
<div
|
||||
className="flex items-center py-1 px-2 rounded cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
onClick={() => handleExpand(node)}
|
||||
>
|
||||
{/* 展开/折叠图标 */}
|
||||
{isLoading ? (
|
||||
<i className="ri-loader-4-line animate-spin text-gray-400 mr-2"></i>
|
||||
) : isExpanded ? (
|
||||
<i className="ri-arrow-down-s-line text-gray-400 mr-2"></i>
|
||||
) : (
|
||||
<i className="ri-arrow-right-s-line text-gray-400 mr-2"></i>
|
||||
)}
|
||||
{/* 节点标签 */}
|
||||
<span className="text-sm flex-1" title={node.label}>
|
||||
{node.label}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
/* 用户节点 */
|
||||
<div className="flex items-center py-1 px-2 rounded hover:bg-gray-100 transition-colors">
|
||||
{/* 用户节点:显示添加按钮 */}
|
||||
<span className="text-sm flex-1" title={node.label}>
|
||||
{node.label}
|
||||
</span>
|
||||
{node.isUser && (
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-2 w-6 h-6 rounded flex items-center justify-center text-sm ${
|
||||
isAdded
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-800 text-white hover:bg-green-700'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddUser(node);
|
||||
}}
|
||||
disabled={isAdded}
|
||||
title={isAdded ? '已添加' : '添加成员'}
|
||||
>
|
||||
{isAdded ? '✓' : '+'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 渲染子节点 - 只有当有实际子节点时才渲染 */}
|
||||
{node.children && node.children.length > 0 && isExpanded && (
|
||||
<div className="ml-4 border-l border-gray-200 pl-2">
|
||||
{option.children?.map(child => (
|
||||
<React.Fragment key={child.value}>
|
||||
{renderOption(child, level + 1)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{node.children.map(child => renderNode(child, level + 1, nodeKey))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染搜索结果
|
||||
const renderSearchResults = () => {
|
||||
if (searchLoading) {
|
||||
return (
|
||||
<div className="p-4 text-center text-gray-400 text-sm">
|
||||
<i className="ri-loader-4-line animate-spin text-lg block mb-1"></i>
|
||||
搜索中...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
return (
|
||||
<div className="p-4 text-center text-gray-400 text-sm">
|
||||
<i className="ri-search-line text-lg block mb-1"></i>
|
||||
无匹配结果
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="text-xs text-gray-500 mb-2 px-1">
|
||||
找到 {searchResults.length} 个用户
|
||||
</div>
|
||||
{searchResults.map(user => (
|
||||
<div
|
||||
key={user.value}
|
||||
className="flex items-center justify-between py-2 px-2 hover:bg-gray-50 rounded"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 truncate">
|
||||
{user.userInfo?.nick_name || user.label}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 truncate">
|
||||
{/* 优先使用 organization_path 中的值(懒加载接口),为空则使用顶层字段(搜索接口) */}
|
||||
{user.userInfo?.organization_path?.tenant_name || user.userInfo?.tenant_name} · {user.userInfo?.ou_name}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddSearchUser(user)}
|
||||
className={`ml-2 px-3 py-1 text-xs rounded ${
|
||||
isUserAdded(user.value)
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-green-800 text-white hover:bg-green-700'
|
||||
}`}
|
||||
disabled={isUserAdded(user.value)}
|
||||
>
|
||||
{isUserAdded(user.value) ? '已添加' : '添加'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 显示文本
|
||||
const getDisplayText = () => {
|
||||
if (selectedUsers.length === 0) return placeholder;
|
||||
return `已选择 ${selectedUsers.length} 位成员`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative inline-block w-full">
|
||||
{/* 触发器 */}
|
||||
<div
|
||||
className="border border-gray-300 rounded px-3 py-2 cursor-pointer bg-white flex justify-between items-center focus:outline-none focus:ring-2 focus:ring-[var(--color-primary,#00684a)] focus:border-[var(--color-primary,#00684a)]"
|
||||
role="button"
|
||||
@@ -261,13 +414,15 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
}}
|
||||
onClick={() => setVisible(!visible)}
|
||||
>
|
||||
<span className={selected.length === 0 ? 'text-gray-400' : ''}>
|
||||
<span className={selectedUsers.length === 0 ? 'text-gray-400' : ''}>
|
||||
{getDisplayText()}
|
||||
</span>
|
||||
<span className="text-gray-400">
|
||||
<i className={visible ? "ri-arrow-up-s-line" : "ri-arrow-down-s-line"}></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 下拉面板 */}
|
||||
{visible && (
|
||||
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 w-full">
|
||||
{/* 搜索框 */}
|
||||
@@ -278,10 +433,10 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
<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)]"
|
||||
className="w-full pl-8 pr-8 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)}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{searchKeyword && (
|
||||
@@ -291,6 +446,7 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchKeyword('');
|
||||
setSearchResults([]);
|
||||
}}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
@@ -299,14 +455,16 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 选项列表 */}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="p-2 overflow-y-auto" style={{ maxHeight: `${maxHeight}px` }}>
|
||||
{filteredOptions.length > 0 ? (
|
||||
filteredOptions.map(option => renderOption(option))
|
||||
{searchKeyword && searchable ? (
|
||||
renderSearchResults()
|
||||
) : localOptions.length > 0 ? (
|
||||
localOptions.map(option => renderNode(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>
|
||||
@@ -316,4 +474,4 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiCascader;
|
||||
export default MultiCascader;
|
||||
|
||||
Reference in New Issue
Block a user