diff --git a/src/components/AccessDepartmentSelect/index.tsx b/src/components/AccessDepartmentSelect/index.tsx index ec3b0ce..7fe8e49 100644 --- a/src/components/AccessDepartmentSelect/index.tsx +++ b/src/components/AccessDepartmentSelect/index.tsx @@ -1,109 +1,414 @@ -import React, { useEffect, useState } from 'react'; -import { TreeSelect, Spin } from 'antd'; -import { treeData } from './services'; // 你的接口 +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Modal, Tree, Input, Spin } from 'antd'; +import { CloseCircleOutlined } from '@ant-design/icons'; +import { treeData, getTreePage } from './services'; export interface AccessDepartmentSelectProps { value?: string | number; onChange?: (value: string | number, label: any, extra: any) => void; + onSelect?: (value: (string | number)[]) => void; placeholder?: string; disabled?: boolean; orgCategory?: string; style?: React.CSSProperties; + useModal?: boolean; + orgIdType?: boolean; } -// 树节点类型 -interface OrgNode { +type RawNode = { orgId: string; orgName: string; - children?: OrgNode[]; - isLeaf?: boolean; - key?: string; - title?: string; orgCategory?: string; -} + children?: RawNode[]; +}; + +type TreeNode = { + title: string; + key: string; + value: string; + disabled?: boolean; + children?: TreeNode[]; +}; + +const PAGE_SIZE = 50; const AccessDepartmentSelect: React.FC = ({ value, onChange, + onSelect, placeholder = '请选择准入部门', disabled = false, style = { width: 200 }, - orgCategory = 'Org' + orgCategory = 'Org', + useModal = true, + orgIdType = true, }) => { - const [tree, setTree] = useState([]); + // 输入框 / 弹窗控制 + const [open, setOpen] = useState(false); + const [hover, setHover] = useState(false); + + // loading const [loading, setLoading] = useState(false); - // 转换接口为 TreeSelect 结构 - function formatTree(data: OrgNode[]): any[] { - return data.map(item => ({ - ...item, - title: item.orgName, - value: item.orgId, - key: item.orgId, - disabled: orgCategory === '' && item.orgCategory === 'Org', - // isLeaf: item.isLeaf || false, - // children: item.children && item.children.length > 0 ? formatTree(item.children) : undefined, + // 展开控制 + const [expandedKeys, setExpandedKeys] = useState([]); + const [autoExpandParent, setAutoExpandParent] = useState(false); + + // 搜索词(空=浏览模式;非空=搜索模式) + const [search, setSearch] = useState(''); + + // 浏览模式:懒加载树 + const [origin, setOrigin] = useState([]); + + // 搜索模式:全量树(分页累加) + const [searchForest, setSearchForest] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const paginatingRef = useRef(false); + + // 索引(显示选中名 & 展开父链) + const keyTitleMapRef = useRef>({}); + const parentKeysMapRef = useRef>({}); + + // ===== 工具:格式化 & 建索引 ===== + const formatTree = (data: RawNode[], lazy = false): TreeNode[] => + (data || []).map((x) => ({ + title: x.orgName, + key: String(x.orgId), + value: String(x.orgId), + disabled: orgCategory === '' && x.orgCategory === 'Org', + // 懒加载模式下,先标成非叶子,这样会有下拉箭头 + isLeaf: lazy ? false : !(x.children && x.children.length), + children: x.children && x.children.length ? formatTree(x.children, lazy) : undefined, })); - } - // 懒加载节点 - const onLoadData = async (treeNode: any) => { - if (treeNode.children && treeNode.children.length > 0) return; - setLoading(true); - const params: any = { upOrgId: treeNode.value }; - if(orgCategory) { - params.orgCategory = orgCategory - } - - const { code, data } = await treeData(params); - if (code === 200 && Array.isArray(data)) { - setTree(origin => updateNode(origin, treeNode.value, formatTree(data))); - } - setLoading(false); + const buildIndex = (forest: TreeNode[]) => { + const titleMap: Record = {}; + const parentMap: Record = {}; + const dfs = (nodes: TreeNode[], parents: string[]) => { + nodes.forEach((n) => { + titleMap[n.key] = n.title; + parentMap[n.key] = parents; + if (n.children && n.children.length) dfs(n.children, [...parents, n.key]); + }); + }; + dfs(forest, []); + keyTitleMapRef.current = titleMap; + parentKeysMapRef.current = parentMap; }; - // 递归插入children - function updateNode(list: OrgNode[], key: string, children: OrgNode[]): OrgNode[] { - return list.map(node => { - if (node.orgId === key) return { ...node, children }; - if (node.children) return { ...node, children: updateNode(node.children, key, children) }; - return node; - }); - } - - // 初始化 + // ===== 初始化:根节点 + 默认展开两级 ===== useEffect(() => { - setLoading(true); - const orgIdStr = sessionStorage.getItem('Userinfo'); - const currentUser = orgIdStr ? JSON.parse(orgIdStr) : null; - const params: any = { orgId: currentUser.organizationId }; - if(orgCategory) { - params.orgCategory = orgCategory - } - treeData(params).then(res => { - if (res.code === 200 && Array.isArray(res.data)) { - setTree(formatTree(res.data)); + (async () => { + setLoading(true); + try { + const orgIdStr = sessionStorage.getItem('Userinfo'); + const currentUser = orgIdStr ? JSON.parse(orgIdStr) : null; + const params: any = { }; + if (orgIdType) { + params.orgId = currentUser?.organizationId + } + if(orgCategory) { + params.orgCategory = orgCategory + } + const res = await treeData(params); + const list: RawNode[] = Array.isArray(res?.data) ? res.data : []; + const tree = formatTree(list, true); + setOrigin(tree); + buildIndex(tree); + + // 默认展开两级 + // const expand: string[] = []; + // const walk = (nodes: TreeNode[], depth: number) => { + // nodes.forEach((n) => { + // if (depth < 2) expand.push(n.key); + // if (n.children) walk(n.children, depth + 1); + // }); + // }; + // walk(tree, 0); + setExpandedKeys([]); + setAutoExpandParent(true); + } finally { + setLoading(false); } - setLoading(false); + })(); + }, [orgCategory]); + + // ===== 懒加载(浏览模式)===== + const findNode = (forest: TreeNode[], key: string): TreeNode | undefined => { + for (const n of forest) { + if (n.key === key) return n; + if (n.children) { + const f = findNode(n.children, key); + if (f) return f; + } + } + return undefined; + }; + const insertChildren = (forest: TreeNode[], key: string, children: TreeNode[]): TreeNode[] => + forest.map((n) => { + if (n.key === key) { + // 如果没有子节点,标记成叶子以移除箭头;否则保持非叶子 + const has = children && children.length > 0; + return { ...n, children, isLeaf: !has }; + } + if (n.children) return { ...n, children: insertChildren(n.children, key, children) }; + return n; }); - }, []); + + const loadChildren = async (nodeKey: string) => { + const already = findNode(origin, nodeKey)?.children; + if (already && already.length) return; + + setLoading(true); + try { + const params: any = { upOrgId: nodeKey, orgCategory }; + const res = await treeData(params); + const list: RawNode[] = Array.isArray(res?.data) ? res.data : []; + const children = formatTree(list, true); + const next = insertChildren(origin, nodeKey, children); + setOrigin(next); + buildIndex(next); + } finally { + setLoading(false); + } + }; + + // ===== 搜索模式:分页拉取 ===== + const mergeForest = (a: TreeNode[], b: TreeNode[]): TreeNode[] => { + // 搜索接口通常返回一组“根节点们”,这里简单用 key 去重合并 + const map = new Map(); + const put = (arr: TreeNode[]) => arr.forEach((n) => map.set(n.key, n)); + put(a); + put(b); + return Array.from(map.values()); + }; + + const fetchSearchPage = async (nextPage = 1, kw = search) => { + paginatingRef.current = true; + try { + const res = await getTreePage({ + orgName: kw, + orgCategory, + basePageRequest: { + pageNo: nextPage, + pageSize: PAGE_SIZE, + }, + }); + const list: RawNode[] = Array.isArray(res?.data.records) ? res.data.records : []; + const pageTrees = formatTree(list); + + const nextForest = nextPage === 1 ? pageTrees : mergeForest(searchForest, pageTrees); + setSearchForest(nextForest); + buildIndex(nextForest); + + setPage(nextPage); + setHasMore(list.length >= PAGE_SIZE); + + // 初页:展开两级 + 有子孙的父节点 + if (nextPage === 1) { + // const expandSet = new Set(); + // const mark = (nodes: TreeNode[], depth: number) => { + // nodes.forEach((n) => { + // if (depth < 2) expandSet.add(n.key); + // if (n.children && n.children.length) { + // expandSet.add(n.key); + // mark(n.children, depth + 1); + // } + // }); + // }; + // mark(pageTrees, 0); + // setExpandedKeys(Array.from(expandSet)); + setExpandedKeys([]); + setAutoExpandParent(true); + } + } finally { + paginatingRef.current = false; + } + }; + //输入完后 点击搜索触发 + const handleSearchConfirm = async (kw: string) => { + const v = (kw || '').trim(); + setExpandedKeys([]); + if (!v) { + // 回到浏览模式 + setSearch(''); + setSearchForest([]); + setAutoExpandParent(true); + return; + } + setSearch(v); + setLoading(true); + try { + await fetchSearchPage(1, v); // 原有分页拉取 + } finally { + setLoading(false); + } + }; + // ===== 触底加载(仅搜索模式)===== + const handlePanelScroll: React.UIEventHandler = async (e) => { + if (!search.trim() || !hasMore || paginatingRef.current) return; + const el = e.currentTarget; + const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 12; + if (!nearBottom) return; + + setLoading(true); + try { + await fetchSearchPage(page + 1, search); + } finally { + setLoading(false); + } + }; + + // ===== 标题高亮(简单包含匹配)===== + const titleRender = (node: any) => { + const t: string = node.title; + if (!search.trim()) return t; + const s = search.trim().toLowerCase(); + const i = t.toLowerCase().indexOf(s); + if (i < 0) return t; + return ( + + {t.slice(0, i)} + {t.slice(i, i + s.length)} + {t.slice(i + s.length)} + + ); + }; + + // ===== 选择(单选)===== + const doChange = (node: TreeNode | null) => { + const val = node ? node.value : (undefined as any); + const label = node ? node.title : undefined; + const extra = node ? { triggerNode: node } : undefined; + onChange?.(val, label, extra); + }; + + const handleSelect = (_keys: any, info: any) => { + onSelect?.(_keys) + const node: TreeNode | undefined = info?.node + ? { title: info.node.title, key: info.node.key, value: info.node.key, children: info.node.children } + : undefined; + if (node) { + doChange(node); + if (useModal) setOpen(false); + } + }; + + // ===== 展开:浏览模式时懒加载子节点 ===== + const onExpand = async (ks: React.Key[], info: any) => { + setExpandedKeys(ks); + setAutoExpandParent(false); + + if (!search.trim() && info?.expanded) { + await loadChildren(String(info.node.key)); + } + }; + + // 清空 + const handleClear = () => doChange(null); + + // 输入框显示的名称 + const selectedName = useMemo(() => { + if (!value) return ''; + return keyTitleMapRef.current[String(value)] || ''; + }, [value, origin, searchForest]); + + // ====== 可复用的选择面板 ====== + const panel = ( + <> + setSearch(e.target.value || '')} // 只更新状态 + onSearch={(kw) => handleSearchConfirm(kw)} // 用户确认时再查 + style={{ marginBottom: 8 }} + /> +
+ + + +
+ + ); return ( - - - + <> + {/* 输入框(悬停显示清空) */} + {useModal && ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + > + !disabled && setOpen(true)} + style={{ paddingRight: '30px', boxSizing: 'border-box' }} + // suffix={} + /> + {hover && !!value && !disabled && ( + { + e.stopPropagation(); + handleClear(); + }} + style={{ + position: 'absolute', + right: 10, + top: '50%', + transform: 'translateY(-50%)', + color: 'rgba(0,0,0,.45)', + cursor: 'pointer', + zIndex: 1, + }} + /> + )} +
+ )} + {useModal ? ( + setOpen(false)} + footer={null} // 单选不需要确定/取消 + destroyOnClose + width={720} + > + {panel} + + ) : ( +
{panel}
+ )} + ); }; diff --git a/src/components/AccessDepartmentSelect/services.ts b/src/components/AccessDepartmentSelect/services.ts index b2a44e4..aa33ac5 100644 --- a/src/components/AccessDepartmentSelect/services.ts +++ b/src/components/AccessDepartmentSelect/services.ts @@ -12,6 +12,21 @@ interface treeInterface { export const treeData = (params: treeInterface) => request.get('/org/list', { params }); +/** + * 查询树 + */ +interface getPageinterface { + orgName?: string; + orgCategory?: string; //Org + basePageRequest: basePage +} +interface basePage { + pageNo: number; + pageSize: number; +} +export const getTreePage = (data: getPageinterface) => request.post('/org/getPage', { data }); + + /** * 部门 */ diff --git a/src/components/TreeCategorySelector/index.tsx b/src/components/TreeCategorySelector/index.tsx new file mode 100644 index 0000000..aa67249 --- /dev/null +++ b/src/components/TreeCategorySelector/index.tsx @@ -0,0 +1,437 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Modal, Tree, Input, Spin } from 'antd'; +import { CloseCircleOutlined } from '@ant-design/icons'; +import { categoryTree } from './services'; + +const { Search } = Input; + +interface Props { + value?: React.Key[]; + onChange?: (value: React.Key[]) => void; + onChangeDetail?: (payload: { + categoryIds: string[]; + coscoAccessCategoryList: { + categoryId: string; + categoryName: string; + categoryPathName: string; + categoryPathId: string; + }[]; + }) => void; + placeholder?: string; + disabledCategories?: string[] + title?: string; + height?: number; + multiple?: boolean; +} + +type TreeNode = { key: string; title: string; children?: TreeNode[], disabled: boolean }; + +const TreeCategorySelector: React.FC = ({ + value = [], + onChange, + onChangeDetail, + placeholder = '请选择品类', + title = '品类选择', + height = 320, + multiple = true, + disabledCategories = [] +}) => { + const [open, setOpen] = useState(false); + + // 原始树(完整)、展示树(过滤后用于渲染) + const [origin, setOrigin] = useState([]); + const [displayTree, setDisplayTree] = useState([]); + const [loading, setLoading] = useState(false); + + // 展开 & 搜索 + const [expandedKeys, setExpandedKeys] = useState([]); + const [autoExpandParent, setAutoExpandParent] = useState(false); + const [search, setSearch] = useState(''); + const searchTimer = useRef(null); + + // 悬浮清空 + const [hover, setHover] = useState(false); + + // 多选临时勾选 / 单选临时选中 + const [checkedAllKeys, setCheckedAllKeys] = useState([]); + const [selectedKey, setSelectedKey] = useState(null); + + // 索引 + const keyTitleMapRef = useRef>({}); + const parentKeysMapRef = useRef>({}); + const leafSetRef = useRef>(new Set()); + + // 默认“两级展开” + const initialExpandedKeysRef = useRef([]); + + // 归一化 + 子序列匹配(逐字符,不要求连续;支持路径) + const norm = (s: string = '') => s.toLowerCase().replace(/[\s/\\>_-]+/g, ''); + const isSubsequence = (q: string, t: string) => { + const qq = norm(q), tt = norm(t); + if (!qq) return true; + let i = 0, j = 0; + while (i < qq.length && j < tt.length) { + if (qq[i] === tt[j]) i++; + j++; + } + return i === qq.length; + }; + + // 构建树 & 索引(只在数据到达时) + const buildTreeOnce = (data: any[]): TreeNode[] => { + const keyTitleMap: Record = {}; + const parentKeysMap: Record = {}; + const leafSet = new Set(); + const collectExpand: string[] = []; + + const dfs = (list: any[], parents: string[], depth: number): TreeNode[] => + (list || []).map((it: any) => { + const key = String(it.id); + const node: TreeNode = { + key, + title: it.categoryName, + disabled: disabledCategories.includes(key), + }; + keyTitleMap[key] = node.title; + parentKeysMap[key] = parents; + if (depth < 1 /* 前两层 */ && Array.isArray(it.children) && it.children.length > 0) { + collectExpand.push(key); + } + if (Array.isArray(it.children) && it.children.length > 0) { + node.children = dfs(it.children, [...parents, key], depth + 1); + } else { + leafSet.add(key); + } + return node; + }); + + const tree = dfs(data, [], 0); + keyTitleMapRef.current = keyTitleMap; + parentKeysMapRef.current = parentKeysMap; + leafSetRef.current = leafSet; + initialExpandedKeysRef.current = Array.from(new Set(collectExpand)); + console.log(tree, 'tree'); + + return tree; + + }; + + // 过滤树:只保留命中节点(路径子序列匹配)及其祖先/包含命中子孙的分支 + const filterTreeForSearch = (nodes: TreeNode[], query: string) => { + if (!query) return { tree: nodes, expandKeys: initialExpandedKeysRef.current }; + const expand = new Set(); + + const walk = (list: TreeNode[], parents: string[]): TreeNode[] => { + const res: TreeNode[] = []; + for (const n of list) { + const fullIds = [...parents, n.key]; + const pathRaw = fullIds.map((id) => keyTitleMapRef.current[id] || '').join('/'); + const hitSelfOrPath = isSubsequence(query, pathRaw); + + let keptChildren: TreeNode[] | undefined; + if (n.children && n.children.length) { + keptChildren = walk(n.children, fullIds); + } + + if (hitSelfOrPath || (keptChildren && keptChildren.length)) { + if (keptChildren && keptChildren.length) expand.add(n.key); // 展开有命中子孙的父节点 + res.push({ ...n, children: keptChildren }); + } + } + return res; + }; + + const tree = walk(nodes, []); + return { tree, expandKeys: Array.from(expand) }; + }; + + // 拉数据 + useEffect(() => { + setLoading(true); + console.log(1); + + categoryTree() + .then((res: any) => { + if (res?.code === 200) { + const tree = buildTreeOnce(res.data || []); + setOrigin(tree); + setDisplayTree(tree); + // 初始展开 + setExpandedKeys(initialExpandedKeysRef.current); + setAutoExpandParent(true); + } + }) + .finally(() => setLoading(false)); + }, []); + + // 打开时同步外部值 + 默认两级展开 + useEffect(() => { + if (!open) return; + if (multiple) setCheckedAllKeys(Array.isArray(value) ? value : []); + else setSelectedKey(value && value.length ? value[0] : null); + + setSearch(''); + setDisplayTree(origin); + setExpandedKeys(initialExpandedKeysRef.current); + setAutoExpandParent(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, multiple]); + + // 清理搜索定时器 + useEffect(() => { + return () => { + if (searchTimer.current) { + window.clearTimeout(searchTimer.current); + searchTimer.current = null; + } + }; + }, []); + + // 当 disabledCategories 改变时,更新树数据的 disabled 状态 + useEffect(() => { + if (!origin.length) return; + if(disabledCategories.length !== 0) { + const updateDisabled = (nodes: TreeNode[]): TreeNode[] => + nodes.map((n) => ({ + ...n, + disabled: disabledCategories.includes(n.key), + children: n.children ? updateDisabled(n.children) : undefined + })); + + const newTree = updateDisabled(origin); + setOrigin(newTree); + + // 保证搜索/展示树也同步 + if (!search.trim()) { + setDisplayTree(newTree); + } else { + const { tree, expandKeys } = filterTreeForSearch(newTree, search); + setDisplayTree(tree); + setExpandedKeys(expandKeys); + } + } + + }, [disabledCategories]); + + // 输入框显示名称(缺的就是它) + const selectedNames = useMemo( + () => (value as React.Key[]) + .map((k) => keyTitleMapRef.current[String(k)] || String(k)) + .filter(Boolean), + [value] + ); + + // 搜索:逐字符匹配 + 只显示命中分支 + const onSearchChange = (val: string) => { + const searchValue = val ?? ''; + setSearch(searchValue); + + if (searchTimer.current) window.clearTimeout(searchTimer.current); + searchTimer.current = window.setTimeout(() => { + const { tree, expandKeys } = filterTreeForSearch(origin, searchValue); + setDisplayTree(tree); + setExpandedKeys(expandKeys); + setAutoExpandParent(true); + }, 120); + }; + + // 高亮(节点自身标题逐字符高亮) + const titleRender = (node: any) => { + const t: string = node.title as string; + if (!search) return t; + const q = norm(search); + if (!q) return t; + + let i = 0; + const spans: React.ReactNode[] = []; + for (let j = 0; j < t.length; j++) { + const ch = t[j]; + if (i < q.length && norm(ch) === q[i]) { + spans.push( + {ch} + ); + i++; + } else { + spans.push({ch}); + } + } + return {spans}; + }; + + // 多选勾选(只回叶子 + 明细) + const handleCheck = (ck: any) => { + const rawKeys: React.Key[] = Array.isArray(ck) ? ck : ck.checked; + setCheckedAllKeys(rawKeys); + + const onlyLeaf = rawKeys.map(String).filter((k) => leafSetRef.current.has(k)); + const coscoAccessCategoryList = onlyLeaf.map((id) => { + const parents = parentKeysMapRef.current[id] || []; + const fullIds = [...parents, id]; + const names = fullIds.map((kid) => keyTitleMapRef.current[kid] || ''); + return { + categoryId: id, + categoryName: keyTitleMapRef.current[id] || '', + categoryPathName: names.join(' / '), + categoryPathId: fullIds.join(','), + }; + }); + + onChange?.(onlyLeaf); + onChangeDetail?.({ categoryIds: onlyLeaf, coscoAccessCategoryList }); + }; + + // 单选:点即回传并关闭 + const handleSelect = (_keys: any, info: any) => { + const id = String(info?.node?.key); + const parents = parentKeysMapRef.current[id] || []; + const fullIds = [...parents, id]; + const names = fullIds.map((kid) => keyTitleMapRef.current[kid] || ''); + const list = [{ + categoryId: id, + categoryName: keyTitleMapRef.current[id] || '', + categoryPathName: names.join(' / '), + categoryPathId: fullIds.join(','), + }]; + + setSelectedKey(id); + onChange?.([id]); + onChangeDetail?.({ categoryIds: [id], coscoAccessCategoryList: list }); + + setOpen(false); + resetTemp(); + }; + + // 多选:点标题切换展开(不选中) + const handleSelectExpand = (_keys: any, info: any) => { + const key = info?.node?.key as string; + setExpandedKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key])); + setAutoExpandParent(false); + }; + + const handleOk = () => { + const onlyLeaf = (checkedAllKeys as React.Key[]).map(String).filter((k) => leafSetRef.current.has(k)); + const coscoAccessCategoryList = onlyLeaf.map((id) => { + const parents = parentKeysMapRef.current[id] || []; + const fullIds = [...parents, id]; + const names = fullIds.map((kid) => keyTitleMapRef.current[kid] || ''); + return { + categoryId: id, + categoryName: keyTitleMapRef.current[id] || '', + categoryPathName: names.join(' / '), + categoryPathId: fullIds.join(','), + }; + }); + + onChange?.(onlyLeaf); + onChangeDetail?.({ categoryIds: onlyLeaf, coscoAccessCategoryList }); + + setOpen(false); + resetTemp(); + }; + + const handleClear = () => { + onChange?.([]); + onChangeDetail?.({ categoryIds: [], coscoAccessCategoryList: [] }); + }; + + const resetTemp = () => { + setSearch(''); + setDisplayTree(origin); + setExpandedKeys(initialExpandedKeysRef.current); + setAutoExpandParent(true); + setSelectedKey(null); + }; + + return ( + <> + {/* 输入框(悬停显示清空) */} +
setHover(true)} + onMouseLeave={() => setHover(false)} + > + setOpen(true)} + style={{ paddingRight: '30px', boxSizing: 'border-box' }} + // suffix={} + /> + {hover && (value?.length ?? 0) > 0 && ( + { + e.stopPropagation(); + handleClear(); + }} + style={{ + position: 'absolute', + right: 10, + top: '50%', + transform: 'translateY(-50%)', + color: 'rgba(0,0,0,.45)', + cursor: 'pointer', + zIndex: 1, + }} + /> + )} +
+ + { + setOpen(false); + resetTemp(); + }} + okText="确定" + cancelText="取消" + destroyOnClose + width={720} + footer={multiple ? undefined : null} + > + + setSearch(e.target.value)} // 只更新本地状态 + onSearch={(val) => onSearchChange(val)} // 回车/按钮点击才执行原逻辑 + value={search} + style={{ marginBottom: 8 }} + /> +
+ { + setExpandedKeys(ks as React.Key[]); + setAutoExpandParent(false); + }} + selectable + checkable={multiple} + onCheck={multiple ? handleCheck : undefined} + checkedKeys={multiple ? checkedAllKeys : undefined} + selectedKeys={multiple ? [] : (selectedKey ? [selectedKey] : [])} + onSelect={multiple ? handleSelectExpand : handleSelect} + /> +
+
+
+ + ); +}; + +export default TreeCategorySelector; diff --git a/src/components/TreeCategorySelector/services.ts b/src/components/TreeCategorySelector/services.ts new file mode 100644 index 0000000..a4092f2 --- /dev/null +++ b/src/components/TreeCategorySelector/services.ts @@ -0,0 +1,7 @@ +import request from '@/utils/request'; + +/** + * 品类选择查询树 + */ +export const categoryTree = () => request.get('/cosco/category/categoryTree'); + \ No newline at end of file diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 87daf94..c264672 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -53,7 +53,7 @@ export default { 'menu.workbenches': '工作台', 'menu.cooperateEnterprise': '企业品类名录', 'menu.changeProgressInquiry': '变更进度查询', - 'menu.supplierNews': '供应商消息', + 'menu.supplierNews': '消息通知', 'menu.informationRetrieval': '供应商信息检索', 'menu.registrationQuery': '注册供应商查询', 'menu.groupQualifiedSupplierQuery': '集团合格供应商查询', diff --git a/src/pages/supplier/ViewReviewPage/index.tsx b/src/pages/supplier/ViewReviewPage/index.tsx index 101a5cc..c1ade39 100644 --- a/src/pages/supplier/ViewReviewPage/index.tsx +++ b/src/pages/supplier/ViewReviewPage/index.tsx @@ -1,55 +1,83 @@ // ViewReviewPage.tsx import React, { useState, useEffect } from 'react'; -import { useLocation } from 'umi'; // 或者 'react-router-dom' import ResultModal from './components/ResultModal'; import ViewModal from './components/ViewModal'; import { refreshDictCache } from '@/servers/api/login'; -import { encryptWithRsa } from '@/utils/encryptWithRsa' - +import { encryptWithRsa } from '@/utils/encryptWithRsa'; const ViewReviewPage: React.FC = () => { - const [modalVisible, setModalVisible] = useState(false); // 控制弹窗 - const [modalRecord, setModalRecord] = useState(null); - + const [modalVisible, setModalVisible] = useState(false); + const [modalRecord, setModalRecord] = useState<{ id: string } | null>(null); + const [type, setType] = useState(''); // 空串即可 useEffect(() => { - const params = new URLSearchParams(location.search); + const params = new URLSearchParams(window.location.search); const base64 = params.get('code'); - console.log(base64); - if (!base64) return; - // 解码 - const decodedStr = atob(base64); - // 再次转成参数 - const p2 = new URLSearchParams(decodedStr); - if (p2.get('id')) { + + try { + // 解码 + const decodedStr = atob(base64); + const p2 = new URLSearchParams(decodedStr); + + const id = p2.get('id') ?? ''; + const code = p2.get('code') ?? ''; + const userId = p2.get('userId') ?? ''; + + if (!id) return; + + setType(code); // code 现在一定是 string + + // 初始化字典 if (!sessionStorage.getItem('dict')) { refreshDictCache().then((res) => { - if (res.code == 200) { - sessionStorage.setItem('dict', JSON.stringify(res.data)) + if (res?.code === 200) { + sessionStorage.setItem('dict', JSON.stringify(res.data)); } - }) + }); } - sessionStorage.setItem('userId', encryptWithRsa(p2.get('userId'))) - setModalRecord({ id: p2.get('id') }); + + // 只有在 userId 存在时再加密保存 + if (userId) { + sessionStorage.setItem('userId', encryptWithRsa(userId)); + } + + setModalRecord({ id }); setModalVisible(true); + } catch (err) { + // atob 或者 URLSearchParams 解析失败 + console.error('解析 code 失败:', err); } }, []); - + /** + * SUPPLIER_ACCESS_APPROVAL("supplierAccessApproval", "供应商准入审批"), + * SUPPLIER_CATEGORY_ACCESS_APPROVAL("supplierCategoryAccessApproval", "供应商品类准入审批"), + * SUPPLIER_INFO_CHANGE_APPROVAL("supplierInfoChangeApproval", "供应商信息变更审批"), + * BLACKLIST_APPROVAL("blacklistApproval", "黑名单审批"), + * EXIT_APPROVAL("exitApproval", "退出审批"), + * CATEGORY_LIBRARY_APPROVAL("categoryLibraryApproval", "品类库审批"), + * CATEGORY_LIBRARY_SUPPLIER_APPROVAL("categoryLibrarySupplierApproval","品类库供应商入库审批"), + * EVALUATION_APPROVAL("evaluationApproval", "评价审批"); + */ + return (
- {/* 其他页面内容 */} - setModalVisible(false)} - /> - setModalVisible(false)} - /> + {/* 供应商准入审批, 供应商品类准入审批 */} + {['supplierAccessApproval', 'supplierCategoryAccessApproval'].includes(type) && ( + <> + setModalVisible(false)} + /> + setModalVisible(false)} + /> + + )}
); }; diff --git a/src/pages/supplier/admission/SupplierCategoryEntry/components/CreateModal.tsx b/src/pages/supplier/admission/SupplierCategoryEntry/components/CreateModal.tsx index 52e81c3..60f70a5 100644 --- a/src/pages/supplier/admission/SupplierCategoryEntry/components/CreateModal.tsx +++ b/src/pages/supplier/admission/SupplierCategoryEntry/components/CreateModal.tsx @@ -1,35 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Form, Button, Tree, message, Input, Spin } from 'antd'; +import { Modal, Form, Button, message, Input } from 'antd'; //组件 import SupplierSelector from './SupplierSelector'; import AccessDepartmentSelect from '@/components/AccessDepartmentSelect'; +import TreeCategorySelector from '@/components/TreeCategorySelector'; + // 请求 -import { categoryTree, add } from '../services'; +import { add } from '../services'; -interface CategoryNode { - key: string; - pathName: string; - path: string; - title: string; - children?: CategoryNode[]; -} - -function getKeyTitlePathMap( - tree: CategoryNode[], - keyTitleMap: Record = {}, - keyPathMap: Record = {}, - keyIdMap: Record = {} - ) { - tree.forEach((node) => { - keyTitleMap[node.key] = node.title; - keyPathMap[node.key] = node.pathName; - keyIdMap[node.key] = node.path; - if (node.children && node.children.length > 0) { - getKeyTitlePathMap(node.children, keyTitleMap, keyPathMap, keyIdMap); - } - }); - return { keyTitleMap, keyPathMap, keyIdMap }; - } // 主体 const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ visible, onCancel }) => { @@ -38,113 +16,29 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi const [accessWorkNameEdited, setAccessWorkNameEdited] = useState(false); //品类选择 - const [checkedKeys, setCheckedKeys] = useState([]); const [disabledCategories, setDisabledCategories] = useState([]); //供应商弹出 const [supplierModalVisible, setSupplierModalVisible] = useState(false); - //品类选择渲染数据 - const [categoriesTreeData, setCategoriesTreeData] = useState([]); //提交防抖 - const [treeLoading, setTreeLoading] = useState(true); const [submitting, setSubmitting] = useState(false); - //品类选择数据中字段转换 - const convertTreeData = (data: any) => { - return data.map((item: any) => ({ - ...item, - title: item.categoryName, - key: item.id, - children: item.children ? convertTreeData(item.children) : undefined, - disabled: disabledCategories.includes(item.id), - })); - } - function findLeafKeys(treeData: any[]): string[] { - let leafKeys: string[] = []; - function dfs(nodes: any[]) { - nodes.forEach(node => { - if (!node.children || node.children.length === 0) { - leafKeys.push(node.key); - } else { - dfs(node.children); - } - }); + + // 辅助函数:根据供应商名和已选品类名生成标题 + const buildAutoTitle = (supplierName: string, categoryNames: string[]) => { + if (supplierName && categoryNames.length) { + return categoryNames.length === 1 + ? `${supplierName}-${categoryNames[0]}-品类准入工作` + : `${supplierName}-${categoryNames[0]}等-品类准入工作`; } - dfs(treeData); - return leafKeys; - } - //品类选择 - const onCheck = ( - checkedKeysValue: - | React.Key[] - | { checked: React.Key[]; halfChecked: React.Key[] } - ) => { - const keys = Array.isArray(checkedKeysValue) - ? checkedKeysValue - : checkedKeysValue.checked; - - // 只取叶子节点 key - const leafKeys = findLeafKeys(convertTreeData(categoriesTreeData)); - const onlyLeafChecked = keys.filter(key => leafKeys.includes(String(key))); - - // 获取映射 - const { keyTitleMap, keyPathMap, keyIdMap } = getKeyTitlePathMap(convertTreeData(categoriesTreeData)); - - // 拼 categoryItem 数组 - const coscoAccessCategoryList = onlyLeafChecked.map((id) => ({ - categoryId: id, - categoryName: keyTitleMap[id] || '', - categoryPathName: keyPathMap[id] || '', - categoryPathId: keyIdMap[id] || '', - })); - - setCheckedKeys(keys); // UI 显示用,还是全量 - form.setFieldsValue({ categoryIds: onlyLeafChecked, coscoAccessCategoryList }); // 只存叶子到表单 - - // ============================== - // 增加自动拼标题的逻辑 - // ============================== - if (!accessWorkNameEdited) { - // 1. 获取供应商名 - const supplier = (form.getFieldValue('supplier') || [])[0]; - let supplierName = ''; - if (supplier) { - supplierName = supplier.supplierType === 'ovs' ? supplier.nameEn : supplier.name; - } - // 2. 获取已选品类名称 - const findNamesByIds = (treeData: any[], ids: any[]) => { - let names: string[] = []; - const dfs = (list: any[]) => { - list.forEach(item => { - if (ids.includes(item.id)) { - names.push(item.categoryName); - } - if (item.children) dfs(item.children); - }); - } - dfs(treeData); - return names; - }; - const categoryNames = findNamesByIds(categoriesTreeData, onlyLeafChecked); - - let autoTitle = supplierName; - if (supplierName && categoryNames.length) { - if (categoryNames.length === 1) { - autoTitle = `${supplierName}-${categoryNames[0]}-品类准入工作`; - } else { - autoTitle = `${supplierName}-${categoryNames[0]}等-品类准入工作`; - } - } else if (!supplierName && categoryNames.length) { - if (categoryNames.length === 1) { - autoTitle = categoryNames[0]; - } else { - autoTitle = `${categoryNames[0]}等-品类准入工作`; - } - } - form.setFieldsValue({ accessWorkName: autoTitle }); + if (!supplierName && categoryNames.length) { + return categoryNames.length === 1 + ? categoryNames[0] + : `${categoryNames[0]}等-品类准入工作`; } + return supplierName || ''; }; //获取 cuCompanyNumber const onChangeDepartmentSelect = (value: any, label: any, extra: any) => { - form.setFieldsValue({ orgId: extra?.triggerNode?.props.cuCompanyNumber }); + form.setFieldsValue({ orgId: extra?.triggerNode?.cuCompanyNumber }); } // 提交 const onFinish = async (values: any) => { @@ -164,8 +58,8 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi orgId: '', }, categoryIds: [], - coscoAccessCategoryList: [], - supplierIds: [], + coscoAccessCategoryList: [], + supplierIds: [], }; //标题名称 finalPayload.coscoAccessWork.accessWorkName = values.accessWorkName; @@ -175,14 +69,12 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi finalPayload.coscoAccessWork.orgId = values.orgId; //品类选择 finalPayload.coscoAccessCategoryList = values.coscoAccessCategoryList; - // finalPayload.categoryIds = values.categoryIds; //选择供应商 if (values.supplier.length != 0) { values.supplier.forEach((item: { id: string }) => { finalPayload.supplierIds.push(item.id) }) } - console.log(finalPayload, values); if (submitting) return; // 防重复提交 setSubmitting(true); @@ -191,7 +83,6 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi if (res?.success) { message.success('创建成功'); form.resetFields(); - setCheckedKeys([]); onCancel(); } else { message.error('创建失败'); @@ -204,17 +95,7 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi //初始化 useEffect(() => { if (visible) { - setTreeLoading(true); - setCheckedKeys([]); setDisabledCategories([]); - categoryTree().then((res) => { - const { code, data } = res; - if (code == 200) { - setCategoriesTreeData(data) - } - }).finally(() => { - setTreeLoading(false); - }); } }, [visible, form]); return ( @@ -224,7 +105,6 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi footer={null} onCancel={() => { form.resetFields(); - setCheckedKeys([]); setDisabledCategories([]); onCancel(); }} @@ -273,33 +153,36 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi {`${form.getFieldValue('supplier') ? form.getFieldValue('supplier')[0].supplierType === 'ovs' ? form.getFieldValue('supplier')[0].nameEn : form.getFieldValue('supplier')[0].name : ''}`} - - + + - {treeLoading ? ( -
- -
- ) : ( - )} + form.setFieldsValue({ categoryIds: ids })} + onChangeDetail={({ categoryIds, coscoAccessCategoryList }) => { + form.setFieldsValue({ categoryIds, coscoAccessCategoryList }); + // ============================== + // 增加自动拼标题的逻辑(新位置) + // ============================== + if (!accessWorkNameEdited) { + // 1) 取供应商名 + const supplier = (form.getFieldValue('supplier') || [])[0]; + const supplierName = supplier + ? (supplier.supplierType === 'ovs' ? supplier.nameEn : supplier.name) + : ''; + + // 2) 直接用 onChangeDetail 传来的名称,避免再遍历整棵树 + const categoryNames = (coscoAccessCategoryList || []).map( + (x: { categoryName: string }) => x.categoryName + ); + + // 3) 生成标题并写回 + const autoTitle = buildAutoTitle(supplierName, categoryNames); + form.setFieldsValue({ accessWorkName: autoTitle }); + } + }} />
@@ -309,8 +192,7 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi + + {/* 回显区域(监听 supplier 字段变化) */} + + {({ getFieldValue }) => { + const supplierValue = getFieldValue('supplier') || []; // [{ id, name, nameEn, supplierType, ... }] + if (!Array.isArray(supplierValue) || supplierValue.length === 0) return null; + return ( +
+ {supplierValue.map((s: any) => { + const name = + s.supplierType === 'ovs' + ? (s.nameEn || s.name) + : (s.name || s.nameEn); + return ( + + {name} + + ); + })} +
+ ); + }} +
+ {admissionMethod === 'offline' && ( <> @@ -447,7 +379,7 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi void; }> = ({ vi + + {({ getFieldValue }) => { + const approversValue = + getFieldValue('reviewers')?.selected || []; + const leader = getFieldValue('reviewers')?.leader || {}; + if (!Array.isArray(approversValue) || approversValue.length === 0) return null; + + return ( +
+ {approversValue.map((s: any) => ( + + {s.name} + {leader.name === s.name && ( + 组长 + )} + + ))} +
+ ); + }} +
{ if (!value || !Array.isArray(value) || value.length === 0) { return Promise.reject(new Error('请设置评审分工')); @@ -508,7 +473,7 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi @@ -554,7 +519,6 @@ const CreateModal: React.FC<{ visible: boolean; onCancel: () => void; }> = ({ vi @@ -279,17 +207,7 @@ const groupQualifiedSupplierQuery: React.FC = ({ dispatch }) => { {!collapsed && ( <>
- - {dataTree.length > 0 && ( - - )} - + handleTreeSelect(val)} />
setCollapsed(true)}>
diff --git a/src/pages/supplier/informationRetrieval/personQualifiedSupplierQuery/index.tsx b/src/pages/supplier/informationRetrieval/personQualifiedSupplierQuery/index.tsx index f75ac2e..91e6075 100644 --- a/src/pages/supplier/informationRetrieval/personQualifiedSupplierQuery/index.tsx +++ b/src/pages/supplier/informationRetrieval/personQualifiedSupplierQuery/index.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useState } from "react"; -import { Form, Button, Table, Input, Tree, Space, Tooltip, Spin } from 'antd'; +import { Form, Button, Table, Input, Space, Tooltip } from 'antd'; import { SearchOutlined, DeleteOutlined, DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; import { connect } from 'umi'; import SupplierViewModal from './components/SupplierViewModal'; import SupplierDetailModal from './components/SupplierDetailModal'; -import { treeData, getPagePe } from './services'; +import { getPagePe } from './services'; import tableProps from '@/utils/tableProps' import { downloadFile } from '@/utils/download'; - -// type OptionType = { label: string; value: string }; +import AccessDepartmentSelect from '@/components/AccessDepartmentSelect/index' + interface Data { id: number; name: string; @@ -26,49 +26,14 @@ interface Data { interface Props { dispatch: any; } -interface TreeNode { - key: string; - title: string; - orgId: string; - orgName: string; - [key: string]: any; - children?: TreeNode[]; -} - -function formatTreeData(nodes: any[]): TreeNode[] { - return nodes.map(node => ({ - ...node, - key: node.orgId, - title: node.orgName - })); -} - -function updateNodeChildren(list: TreeNode[], key: string, children: TreeNode[]): TreeNode[] { - return list.map(item => { - if (item.key === key) return { ...item, children }; - if (item.children) return { ...item, children: updateNodeChildren(item.children, key, children) }; - return item; - }); -} - -const findFirstLeafKey = (nodes: any[]): string | undefined => { - for (const node of nodes) { - if (!node.children) return node.key; - const found = findFirstLeafKey(node.children); - if (found) return found; - } - return undefined; -}; + const PersonQualifiedSupplierQuery: React.FC = ({ dispatch }) => { const [form] = Form.useForm(); - const [dataTree, setDataTree] = useState < TreeNode[] > ([]); - const [treeSelected, setTreeSelected] = useState < string[] > ([]); const [selectedKeys, setSelectedKeys] = useState < string > (''); const [DeptId, setDeptId] = useState < string > (''); const [data, setData] = useState < Data[] > ([]); const [loading, setLoading] = useState(false); - const [treeLoading, setTreeLoading] = useState(false); const [pagination, setPagination] = useState < TablePaginationConfig > ({ current: 1, pageSize: 10, total: 0 }); const [viewVisible, setViewVisible] = useState(false); const [detailVisible, setDetailVisible] = useState(false); @@ -86,29 +51,12 @@ const PersonQualifiedSupplierQuery: React.FC = ({ dispatch }) => { getList(selectedKeys ? selectedKeys : currentUser.organizationId); }; - const handleTreeSelect = (keys: React.Key[]) => { - const key = keys[0] as string; + const handleTreeSelect = (keys: string | number) => { + const key = keys as string; setSelectedKeys(key); - setTreeSelected([key]); getList(key); }; - // 懒加载树节点 - const onLoadTreeData = async (treeNode: any) => { - if (treeNode.children && treeNode.children.length > 0) return; - setTreeLoading(true); - try { - const res = await treeData({ upOrgId: treeNode.orgId, orgCategory: 'Org' }); - const { code, data } = res; - if (code === 200) { - const children = formatTreeData(data); - setDataTree(origin => updateNodeChildren(origin, treeNode.key, children)); - } - } finally { - setTreeLoading(false); - } - }; - const getList = async (orgId: string, pageNo: number = 1, pageSize: number = 10) => { setLoading(true); try { @@ -128,22 +76,7 @@ const PersonQualifiedSupplierQuery: React.FC = ({ dispatch }) => { // 初始化 useEffect(() => { - setTreeLoading(true); - treeData({orgCategory: 'Org'}).then((res) => { - const { code, data } = res; - if (code === 200) { - const tree = formatTreeData(data); - setDataTree(tree); - const firstLeafKey = findFirstLeafKey(tree); - if (firstLeafKey) { - // setSelectedKeys(firstLeafKey); - // setTreeSelected([firstLeafKey]); - getList(currentUser.organizationId); - } - } - }).finally(() => { - setTreeLoading(false); - }); + getList(currentUser.organizationId); }, []); const columns: ColumnsType = [ @@ -223,7 +156,6 @@ const PersonQualifiedSupplierQuery: React.FC = ({ dispatch }) => {