注:请以实际需求为参考。本功能使用 gantt 的商业版本,版本是1.7版本。免费版本有些功能不支持。需求1、需求描述场景:要实现的功能就是单行数据能左右拖动。流程五个:ABCDE。(对应:Charter开发、概念和计划、初样开发、正样开发、验证)1、A有开始时间,结束时间。B的开始时间必须是A的结束时间(至少间隔一天或者大于A的结束时间)。CDE以此类推。2、一条数据有父子级的话,父级数据的进度条取A的开始时间,E的结束时间,如果当前流程没到E,就是取现有的流程的结束时间。流程阶段影响父级3、单个流程进度条移动会影响前后进度条,如果没有前后节点就不影响# Gantt 拖拽逻辑## 1. 拖动第一条数据- **拖动开始时间**:直接更新数据,成功。- **拖动结束时间**:需与下一条数据的结束时间比对: - '拖动结束时间' 大于 '下条数据的结束时间' 不合规则。 - '拖动结束时间' 等于 '下条数据的结束时间' 也不合规,至少要间隔一天。## 2. 拖动最后一条数据- **拖动结束时间**:直接更新数据,成功。- **拖动开始时间**:需与上一条数据的开始时间比对: - '拖动开始时间' 小于 '上条数据的开始时间' 不合规则。 - '拖动开始时间' 等于 '上条数据的开始时间' 也不合规,至少要间隔一天。## 3. 拖动中间数据- **拖动开始时间**: - 不能小于 '上条数据的开始时间',且必须间隔至少一天。 - 不能大于 '下条数据的结束时间',且与 '下条数据的结束时间' 间隔至少一天。- **拖动结束时间**: - 不能大于 '下条数据的结束时间',且必须间隔至少一天。 - 不能小于 '上条数据的开始时间',且与 '上条数据的开始时间' 间隔至少一天。# Gantt 勾选框点击事件处理逻辑## 处理全选逻辑当用户选择全选复选框时,调用此函数更新所有任务的选中状态。@param {boolean} isChecked - 表示全选复选框的选中状态。如果为 true,则表示用户想要选择所有可选的任务;如果为 false,则表示取消选择所有任务。### 逻辑说明1. 获取所有任务。2. 遍历所有任务,更新每个任务的选中状态,排除状态为禁用(`status = 2` 或 `state = 2`)的任务。3. 对于每个父级任务(`type = 1`),检查其是否有未发布的子任务(`status = 1` 或 `state = 1`)。 - 如果有未发布的子任务,则根据全选状态更新父级任务的选中状态。4. 过滤出所有非禁用的任务,更新全选状态下的选择任务列表。2、效果图一、代码实现1、父组件页面import React, { useState, useMemo, useRef, useEffect } from 'react'; import { organizationId, projectAddress } from '@/common/config'; import { ButtonColor, FuncType } from 'choerodon-ui/pro/lib/button/enum'; import { Button, Cascader, DataSet, DatePicker, Form, message, Modal, Pagination, Select, TextField, Tooltip, } from 'choerodon-ui/pro'; import moment from 'moment'; import { LabelLayout } from 'choerodon-ui/pro/lib/form/enum'; import '@/styles/c7n.less'; import './main.less'; import formatterCollections from 'utils/intl/formatterCollections'; import { commonModelPrompt, ipdProductCode, languageConfig, } from '@/language/language'; import { Button as PermissionButton } from 'components/Permission'; import ExcelExport from 'components/ExcelExportPro'; import { exportButtonProps } from '@/utils/main'; import CommonImport from 'components/CommonImport'; import { colorMap, columns, queryListDs, timeScaleList, tooltipTemplate, } from './store'; import { shallowEqual, useSelector } from 'dva'; import { mockTable } from './mockData'; import { flattenChildrenWithParent } from '@/utils/gantt'; import { postSubmitPublish, exportApi, postPlatformCharterList, postUpdateMilestones, } from '@/api/platformCharter/main'; import { SearchItemProps } from '@/interface/platformCharter'; // import GanttModal from './components/Gantt/main'; import GanttModal from '@/components/GanttModal/main'; import Version from './components/Version/main'; import { ErrorMessage, pubPath } from '../detail/hook'; import DynamicColumns, { DynamicSourceEnum, } from '@/components/DynamicColumns/main'; import _ from 'lodash'; import { queryMapIdpValue } from 'services/api'; import { getResponse } from 'utils/utils'; import { getDynamicColumns, saveDynamicColumns, } from '@/api/productCharter/main'; import Loading from '@/components/Loading/index'; const Index = ({ history }) = { const [pageLoading, setPageLoading] = useState(false); const [visible, setVisible] = useState(false); const ganttChecked = useRefany(); // 勾选数据存储 const [_, forceUpdate] = useState({}); // 用于强制刷新 const timeScale = useRef('month'); // 时间刻度 const [timeScaleListData, setTimeScaleListData] = useStateany( timeScaleList, ); /** 分页 */ const [paginationData, setPaginationData] = useState({ number: 0, size: 10, totalElements: 0, }); /** ds 查询 */ const queryBarDs = useMemo(() = new DataSet(queryListDs()), []); /** column */ const [ganttTableColumns, setGanttTableColumns] = useStateany(columns); /** table */ const [tableListData, setTableListData] = useStateany({ data: [], }); /** 当前tab页 */ const activeTabKey = useSelector( (state: any) = state?.global?.activeTabKey, shallowEqual, ); /** * 获取值集数据 * @returns {void} * */ const tableColunmsRef = React.useRef([]); const [tableColunms, setTableColunms] = useState([]); const getLtcCustomerAddress = async (): Promisevoid = { // LTO模块创建地址 const result = await queryMapIdpValue({ 'IPD_PLATFORM_TECHNOLOGY_TABLE-COLUMN': 'IPD_PLATFORM_TECHNOLOGY_TABLE-COLUMN', }); const response = getResponse(result); if (response !response.failed) { let allTableColunms = response['IPD_PLATFORM_TECHNOLOGY_TABLE-COLUMN']; tableColunmsRef.current = allTableColunms; setTableColunms(allTableColunms); } }; useEffect(() = { if (activeTabKey === '/platform-charte') { (async () = { await getLtcCustomerAddress(); await searchList('search'); })(); } }, [activeTabKey]); /** 查询 */ const searchList = async ( operateType, pageNum?: number, pageSize?: number, ) = { setPageLoading(true); const data = queryBarDs.current?.toData() || {}; const { name = '', importTeamCodes = [], milestoneDate = [] } = data; // 查询条件 const searchParams = { name, importTeamCodes: importTeamCodes?.flat()?.join(',') || '', charterInitTime: milestoneDate.startTime || '', endcpTime: milestoneDate.endTime || '', }; // 重置 if (operateType === 'reset') { queryBarDs.current?.reset(); // 清除查询条件 timeScale.current = 'month'; // 时间刻度恢复成'月' handleTimeScaleChange('month'); } // 查询参数 const params: SearchItemProps = { page: pageNum || 0, size: pageSize || 10, ...(operateType === 'search' ? { ...searchParams } : {}), }; // console.log('查询参数', params); // 获取列表 const result = await postPlatformCharterList(params); // console.log('result', result); if (result.failed) { setPageLoading(false); return message.error(result.message, 1.5, 'top'); } if (result?.msg) { ErrorMessage(result?.msg); return; } // 数据处理(甘特图结构需要) const formatResult = flattenChildrenWithParent(result.content, colorMap); // console.log('转换后的数据', formatResult); // 设置表格列 await searchDynamicColumns(); // 赋值 setTableListData({ data: formatResult }); setPaginationData({ number: result.number + 1, size: result.size, totalElements: result.totalElements, }); setPageLoading(false); }; /** 导入 */ const handleImport = () = { // 导入组件属性 const importProps = { sync: false, auto: false, prefixPatch: projectAddress, backPath: '', action: '*数据导入之前请先进行数据验证,过程中需要您耐心等待', pathKey: 'custProductReport', code: 'IPD_PMS_PLATFORM_ROADMAP_TEM', tenantId: organizationId, args: JSON.stringify({ tenantId: organizationId, }), }; Modal.open({ title: languageConfig('btn.import', '导入'), closable: true, destroyOnClose: true, maskClosable: true, children: CommonImport {...importProps} /, autoCenter: true, style: { width: 1200 }, onOk: () = { searchList('search'); }, }); }; /** 按钮组 */ const ButtonGroup = () = { return ( PermissionButton key="versionComparison" type="c7n-pro" funcType={FuncType.raised} color={ButtonColor.default} icon="sim_card_download" permissionList={[ { code: 'ipd.platformCharter.versionComparison.btn', type: 'button', meaning: languageConfig('btn.versionComparison', '版本对比'), }, ]} onClick={() = setVisible(true)} {languageConfig('btn.versionComparison', '版本对比')} /PermissionButton {/* 导入 */} PermissionButton key="import" type="c7n-pro" funcType={FuncType.raised} color={ButtonColor.default} icon="file_upload_black-o" permissionList={[ { code: 'ipd.platformCharter.import.btn', type: 'button', meaning: languageConfig('btn.import', '导入'), }, ]} onClick={() = handleImport()} {languageConfig('btn.import', '导入')} /PermissionButton {/* 导出 */} PermissionButton key="export" type="c7n-pro" funcType={FuncType.raised} color={ButtonColor.default} permissionList={[ { code: 'ipd.platformCharter.export.btn', type: 'button', meaning: languageConfig('btn.export', '导出'), }, ]} ExcelExport queryParams={() = { // return { ...tableDs.queryParameter }; return {}; }} requestUrl={exportApi} otherButtonProps={exportButtonProps} defaultSelectAll={true} exportAsync method={'get'} / /PermissionButton {/* 提交发布 */} PermissionButton key="submitPublish" type="c7n-pro" icon="liebiaodaochu" color={ButtonColor.primary} funcType={FuncType.raised} permissionList={[ { code: 'ipd.platformCharter.submitPublish.btn', type: 'button', meaning: languageConfig('btn.submitPublish', '提交发布'), }, ]} onClick={handleSubmitPublish} {languageConfig('btn.submitPublish', '提交发布')} /PermissionButton / ); }; /** 切换时间刻度 */ const handleTimeScaleChange = (val: string) = { // 更新当前时间刻度 const update = timeScaleListData.map(item = ({ ...item, status: item.name === val, })); setTimeScaleListData(update); timeScale.current = val; }; /** gantt 回调:弹框打开(详情/编辑) */ const handleGantt = (params: any) = { const { operationType, operationId } = params; const basePath = `${pubPath}/platform-charte/details`; const idParam = operationId ? `/${operationId}` : ''; const typeParam = operationType operationType !== 'detail' ? `?type=${operationType}` : ''; const url = `${basePath}${idParam}${typeParam}`; history.push(url); }; /** * 打开动态列 */ const dynamicColumnsRef = useRefany(null); const openDynamicColumns = () = { Modal.open({ title: languageConfig('processLog.label', '动态列'), style: { width: '550px' }, closable: true, maskClosable: false, keyboardClosable: false, bodyStyle: { display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '0', }, okText: languageConfig('btn.save', '保存'), cancelButton: true, children: ( DynamicColumns ref={dynamicColumnsRef} // lookupCode='IPD_PRODUCT_TABLE_COLUMNS' tableColunms={tableColunms} selectedColumns={ganttTableColumns} source={DynamicSourceEnum.PLATFORM} onWatchSelectColumns={watchSelectColumns} / / ), onOk: async () = { const params = dynamicColumnsRef.current; if (!params) { message.error( languageConfig('config.dynamicColumns', '请选择动态列'), 1.5, 'top', ); return false; } const result = await saveDynamicColumns(params); if (result.code !== 200) { message.error(result.msg, 1.5, 'top'); return false; } message.success(result.msg, 1.5, 'top'); dynamicColumnsRef.current = null; searchList('search'); }, onClose: () = { setVisible(false); }, }); }; const watchSelectColumns = (params: any) = { dynamicColumnsRef.current = params; }; /** * 查询动态列 */ const searchDynamicColumns = async () = { const result = await getDynamicColumns({ type: 2 }); if (result.code !== 200) { return message.error(result.msg, 1.5, 'top'); } if (result result.data result.data.length 0) { // 获取第一个列名称 const firstColumnName = result.data?.[0]; // 模板集合 const templateList = { status: (task: any) = { const stateObj = { 1: '未发布', 2: '已发布', }; return `span class=${task.status === 1 ? 'state-no' : 'state-yes'}${ stateObj[task.status] }/span`; }, }; const newGanttTableColumns = tableColunmsRef.current ?.map((item: any) = { if (result.data?.includes(item.value)) { return { name: item.value, label: item.meaning, resize: true, tree: firstColumnName === item.value, template: templateList[item.value], }; } }) .filter(Boolean); setGanttTableColumns(newGanttTableColumns); } }; /** gant 回调:接收子组件的勾选框变化 */ const handleChooseCheckChange = checkList = { console.log('父组件接收的 chooseParams:', checkList); ganttChecked.current = checkList || []; }; /** 提交发布 */ const handleSubmitPublish = async () = { console.log('提交发布::', ganttChecked.current); const list = ganttChecked.current || []; if (list?.length === 0) { message.error( languageConfig( 'platformCharter.tip..pleaseChooseOneData', '请至少选择一条要发布的数据!', ), undefined, undefined, 'top', ); return false; } console.log('list', list); /** 参数 */ let arr: any = []; list.map((item: any) = { arr.push({ id: item.bId, type: item.type, objectVersionNumber: item.objectVersionNumber, }); }); console.log('arr', arr); debugger; const res = await postSubmitPublish(arr); if (res.failed) { message.error(res?.message || '操作失败', 1.5, 'top'); return false; } if (res?.code === 200) { message.success( languageConfig('tip.success.publish', '发布成功'), 1.5, 'top', ); } else { ErrorMessage(res?.msg); } searchList('search'); // 刷新页面 ganttChecked.current = []; // 清除 gantt 中传过来存储勾选数据 forceUpdate({}); // 强制刷新组件 }; /** gantt 里程碑更新 */ const handleMilestones = async val = { console.log('里程碑提交传参', val.params); const { params, list } = val; const res = await postUpdateMilestones(params); if (res.failed) { message.error(res?.message || '操作失败', 1.5, 'top'); return false; } if (res?.code === 200) { // 更新成成'替换'数据 message.success( languageConfig('tip.success.updateMilestones', '里程碑更新成功'), 1.5, 'top', ); setTableListData({ data: list }); } else { // 更新失败'刷新页面' message.error(res?.msg, 1.5, 'top'); searchList('search'); } }; return ( {pageLoading Loading /} div className="ltc-c7n-style" style={ { overflow: 'auto', margin: '16px', borderRadius: '8px', height: '100%', }} div className="ipd_platform_list" div className="ipd_platform_list__content" {/* Header */} div className="ipd_platform_list__content__header" div {languageConfig( 'platformCharter.title. platformRoadmap', '平台路标', )} /div div{ButtonGroup()}/div /div {/* Search */} div className="ipd_platform_list__content__search" div className="ipd_platform_list__content__search__left" Form columns={6} labelLayout={LabelLayout.none} dataSet={queryBarDs} labelWidth={0} TextField clearButton / Cascader searchable dropdownMenuStyle={ { // minWidth: '2rem', maxWidth: '1.8rem', backgroundColor: '#fff', }} / DatePicker range // comboRangeMode colSpan={2} clearButton / Form.Item Button funcType={FuncType.raised} color={ButtonColor.primary} icon="search" onClick={() = searchList('search')} /Button Button funcType={FuncType.raised} icon="refresh" onClick={() = searchList('reset')} /Button Tooltip placement="top" theme="dark" title={languageConfig( 'table.dynamicColumns.column', '配置动态列', )} Button funcType={FuncType.raised} icon="settings-o" onClick={openDynamicColumns} /Button /Tooltip / /Form.Item /Form /div {/* 时间刻度切换按钮 */} div className="ipd_platform_list__content__search__right" {timeScaleListData.map(item = ( div className={`ipd_platform_list__content__search__right__item ${ item.status ? 'ipd_platform_list__content__search__right__item__active' : 'ipd_platform_list__content__search__right__item__disabled' }`} key={item.name} onClick={() = handleTimeScaleChange(item.name)} {item.label} /div ))} /div /div {/* Table */} div className="ipd_platform_list__content__table" GanttModal onSelect={params = handleGantt(params)} onChooseCheckChange={handleChooseCheckChange} onUpdateMilestones={val = handleMilestones(val)} infoData={ { timeScale: timeScale.current, tableColunm: ganttTableColumns, tableData: tableListData, tooltipTextTemplate: tooltipTemplate, }} / /div {/* 分页 */} div className="ipd_platform_list__footer" Pagination showPager showQuickJumper page={paginationData.number} pageSize={paginationData.size} total={paginationData.totalElements} showTotal={(total, range) = `共${total}条`} onChange={(pageNum, pageSize) = { searchList('search', pageNum - 1, pageSize); }} / /div /div {/* 弹框:版本选项 */} {visible ( Version visible={visible} setVisible={setVisible} onSelect={() = {}} title={languageConfig('btn.versionComparison', '版本对比')} infoData={ {}} / )} /div /div / ); }; export default formatterCollect