|
|
import {getDetailList, delDetail, getDetail, getPointByUuid, getPointsById, getAlignPointById} from '../../../services/measure/analysis' import { useState, useEffect, useRef } from 'react' import {message, Button, type TableColumnsType, type TableProps, Modal, Table, Pagination, Input, Select, Switch, Progress, Flex, Spin } from 'antd'; import type { AnalysisReport, DetailTable, SearchParams } from "../../../services/measure/type"; import { ExclamationCircleFilled, CheckCircleOutlined, WarningOutlined } from '@ant-design/icons'; import { AnalysisData, BenchmarkShape, MeasurementCanvasRef } from './konva/MeasurementCanvas'; import MeasurementCanvas from "./konva/MeasurementCanvas"; import { dictionaryListService, kljUpload } from "../../../services/ktj/org"; import { getReport } from "../../../services/measure/analysis"; import { getBaseRecordPointSetByCode } from "../../../services/track/trackShape" import { extraDescType, KTJ_BASE_TYPE } from '../../../services/ktjTypes'; import { GX_CODE } from '../../../constant'; import { exportFile, padNumber } from '../../../utils'; export default function MeasureDetail() { useEffect(()=>{ queryDictionaryList() const params = { pageSize, pageNum, } getDetailDataList(params) }, [])
const [selectRows, setSelectedRow] = useState<DetailTable[]>([]) const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const rowSelection = { selectedRowKeys, onChange: (selectedRowKeys: React.Key[], selectedRows: DetailTable[]) => { console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); setSelectedRow(selectedRows) setSelectedRowKeys(selectedRowKeys) }, getCheckboxProps: (record: DetailTable) => ({ name: record.name, }), };
const columns: TableColumnsType<DetailTable> = [ { title: '序号', dataIndex: 'seq', render:(_, record, index)=>{ return index + 1 } }, { title: '测量名称', dataIndex: 'name', }, { title: '数据来源', dataIndex: 'dataSource', render:(_, record)=>{ const dataSource = record.dataSource return dataSource === 'DCDC' ? '道岔调查' : '线路调查' } }, { title: '轨型', dataIndex: 'railSize', }, { title: '当天测量序号', dataIndex: 'todayNumber', render: (_, record) => ( padNumber(record.todayNumber, 4) ) }, { title: '创建者', dataIndex: 'operator', }, { title: '时间', dataIndex: 'createTime', }, { title: '操作', dataIndex: 'op', width:180, align:'center', render:(_, record)=>{ return <div> <Button type="link" onClick={()=>onShowDetail(record)}>查看结果</Button> <Button type="link" onClick={()=>onDownloadRecord(record)}>导出</Button> </div> } }, ];
const [tableData, setTableData] = useState<DetailTable[]>([]) const [total, setTotal] = useState(0) const getDetailDataList = (params:SearchParams) => { setLoading(true) getDetailList(params).then(res => { if(res.success){ setLoading(false) //@ts-ignore
setTableData(res.data.list) setTotal(res.data.total) }else{ } }).catch(e=>{ }).finally(()=>{ setLoading(false) }) } const [selectionType, setSelectionType] = useState<'checkbox'>('checkbox'); const { confirm } = Modal; const onBatchDel = () => { confirm({ title: '提示', icon: <ExclamationCircleFilled />, content: '请确认是否删除选中的数据', okText:'确认', cancelText:'取消', onOk() { onHandelDelData() }, onCancel() { console.log('Cancel'); }, }); }
let [idList, setIdList] = useState<number[]>([]); let [isModaUploadlOpen, setIsModaUploadlOpen] = useState(false) const onUploadData = () => { setIdList([]) let list = [...selectRows] let Ids:number[] = [] list.map(item => { Ids.push(item.id) }) setIdList(Ids) handleUpload(Ids) setIsModaUploadlOpen(true) }
const onDownloadData = async () => { // 1. 准备 id 列表
const Ids = selectRows.map(r => r.id); // 2. 发起请求
const response = await fetch(`/api/measurement-data/downloads-zip/${Ids.join(',')}`); // 3. 拿到 Blob
const blob = await response.blob(); // 4. 解析后端文件名
const contentDisp = response.headers.get('content-disposition'); const filename = parseFilename(contentDisp) || 'download.zip'; // 5. 触发下载
exportFile(blob, filename); };
const [percent, setPercent] = useState(0) const [failRecordNames, setFailRecordName] = useState<string[]>([]) const [successRecordNames, setSuccessRecordName] = useState<string[]>([]) const handleUpload = (Ids:number[]) => { if(Ids && Ids.length){ kljUpload({ ids:Ids }).then(res => { if(res.status !== 0){ message.error('道岔数据上传失败') }else{ message.success('上传成功') if(res.data.successList && res.data.successList.length){ let list:string[] = [] res.data.successList.map((item:string) => { let recordName = item.split(":")[0] list.push(recordName) }) setSuccessRecordName(list) }
if(res.data.failList && res.data.failList.length){ let list:string[] = [] res.data.failList.map((item:string) => { let recordName = item.split(":")[0] list.push(recordName) }) setFailRecordName(list) } setPercent(100) } }).catch(e=>{ }) } }
//关闭上传
const handleCloseUpload = () => {
}
const [currentRecord, setCurrentRecord] = useState<Partial<DetailTable>>({}) const onShowDetail = async (item:DetailTable)=> { //获取基线
setIsModalOpen(true) setCurrentRecord(item) setshowCalibration(false) let res = await getBaseRecordPointSetByCode(item.railSize) if (res.success && res.data && res.data.points) { const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[]; if(!benchmarkShapes || !benchmarkShapes.length){ message.error('选择的测量记录没有数据') return; } setTimeout(()=>{ if (canvasRef.current) { console.log("解析后的基础图形数据:", benchmarkShapes); canvasRef.current.setBenchmarkData(benchmarkShapes); } },100) } let resData = await getDetail({id:item.id}) if(resData){ // navigate(`/measure/detail/${item.id}`)
//@ts-ignore
// getRecordByUuid(resData.data.uuid)
//@ts-ignore
onDetaiResult(resData.data.uuid) } //获取测量的坐标点
getMeasurePoints(item) }
function parseFilename(disposition: string | null): string | null { if (!disposition) return null; // 匹配 filename*=UTF-8''xxx 或 filename=xxx(带不带引号都能捕获)
const match = disposition.match(/filename\*?=(?:UTF-8'')?["']?([^"';]+)["']?/i); return match ? decodeURIComponent(match[1]) : null; }
//导出
const onDownloadRecord = async (item: DetailTable) => { // 1. 发请求
const response = await fetch(`/api/measurement-data/download/${item.id}`);
// 2. 拿到 Blob
const blob = await response.blob();
// 3. 解析头里的文件名
const contentDisp = response.headers.get('content-disposition'); // fallback 到 item.name + .txt
const filename = parseFilename(contentDisp) || `${item.name}.txt`;
// 4. 触发下载
exportFile(blob, filename); };
const getMeasurePoints = (recordItem:DetailTable) => { getPointsById({id:recordItem.id}).then(res=>{ if (canvasRef.current) { // canvasRef.current.setMeasurementDataLeft(res.data.leftPoints)
// canvasRef.current.setMeasurementDataRight(res.data.rightPoints)
setshowCalibration(true) canvasRef.current?.setMeasurementCalibrationData(res.data.alignPoints) } }) }
//初始化科天健机构数据
let [SJLY_List, setSJLYList] = useState<KTJ_BASE_TYPE[]>([]); const queryDictionaryList = () => { dictionaryListService().then((res) => { if (res && res.data) { const SJLY:KTJ_BASE_TYPE[] = res.data.SJLY if(SJLY && SJLY.length){ let sjlylist = SJLY.filter(item => item.key === 'XLDC' || item.key === 'DCDC') setSJLYList(sjlylist); } } }); };
const onCancelModal = () => { setIsModaUploadlOpen(false) setPercent(0) setFailRecordName([]) setSuccessRecordName([]) }
//法线
const [analysisReport, setAnalysisReport] = useState<AnalysisReport>(); const onDetaiResult = (uuid:string) => { const gxValue= currentRecord.railSize || GX_CODE const params = { uuid, code:gxValue } getReport(params).then(res=> { if (res.success) { const report: AnalysisReport = res.data; console.log(report); // 更新 canvas 的分析数据
if (report && report.angleAnalysisList) { // 先过滤掉 distance 为 null 的数据
const validItems = report.angleAnalysisList.filter(item => item.distance !== null); const analysisData: AnalysisData[] = validItems.map(item => ({ pointA: { x: parseFloat(item.pointA ? item.pointA.x : '0'), y: parseFloat(item.pointA ? item.pointA.y : '0') }, pointB: { x: parseFloat(item.pointB ? item.pointB.x : '0'), y: parseFloat(item.pointB ? item.pointB.y : '0') }, base: { x: parseFloat(item.pointA ? item.pointA.x : '0'), y: parseFloat(item.pointA ? item.pointA.y : '0') }, measure: { x: parseFloat(item.pointB ? item.pointB.x : '0'), y: parseFloat(item.pointB ? item.pointB.y : '0') }, distance: parseFloat(item.distance), describe: item.describe, })); canvasRef.current?.setAnalysisData(analysisData); } setAnalysisReport(report); } else { message.error("分析报告请求失败: " + res.data.info); } }) }
const getRecordByUuid = (uuid:string) => { getPointByUuid({uuid}).then(res=>{ if(res.data && res.data.points){ canvasRef.current?.setMeasurementData(res.data.points); } }) }
type DelParams = { ids: string | number; } const onHandelDelData = () =>{ let list = [...selectRows] const ids = list.map(item => item.id) if(!ids || !ids.length){ return; } const params = { ids:ids.join(',') } doDel(params) }
const doDel = (params:DelParams) => { delDetail(params).then(res => { if(res.success){ message.success('删除成功') const params = { pageSize, pageNum, } getDetailDataList(params) setSelectedRowKeys([]) setSelectedRow([]) } }).catch(e=> { }) }
const [pageNum, setPageNum] = useState(1) const [pageSize, setPageSize] = useState(8) const onPageChange = (pageNumValue:number, pageSizeValue:number) => { setPageNum(pageNumValue) setPageSize(pageSizeValue) searchParams = { ...searchParams, pageSize:pageSizeValue, pageNum:pageNumValue, } getDetailDataList(searchParams) }
const [name, setName] = useState<string>() const [dataSource, setDataSource] = useState<string>() let searchParams:SearchParams = { pageNum, pageSize, } const onSearch = ()=> { setPageNum(1) setPageSize(5) searchParams = { pageSize, pageNum: 1, name, dataSource } getDetailDataList(searchParams) } let [loading, setLoading] = useState(false) let [isModalOpen, setIsModalOpen] = useState(false) const canvasRef = useRef<MeasurementCanvasRef>(null); const [showGrid, setShowGrid] = useState(true); const [angleMarkBackup, setAngleMarkBackup] = useState(true); const [showStandard, setShowStandard] = useState(true); const [showMark, setShowMark] = useState(true); const handleCancel = () => { canvasRef.current?.setAnalysisData([]); canvasRef.current?.setMeasurementData([]); canvasRef.current?.setBenchmarkData([]) //@ts-ignore
setAnalysisReport({}) setIsModalOpen(false) }
//校准
const [showCalibration, setshowCalibration] = useState(false) const [caloading, setCaLoading] = useState(false) const handleCalibration = () => { setCaLoading(true) if(currentRecord && currentRecord.id){ getAlignPointById({id:currentRecord.id}).then(res=>{ if(res.success){ setshowCalibration(true) canvasRef.current?.setMeasurementCalibrationData(res.data) }else{ message.error('校准失败!') } setCaLoading(false) }).catch(e=>{ message.error('校准失败!') }) } }
useEffect(() => { const intervalId = setInterval(() => { if (percent < 100) { const randomIncrement = Math.floor(Math.random() * 3) + 1; const newProgress = Math.min(percent + randomIncrement, 100); setPercent(newProgress); } else { clearInterval(intervalId); } }, 1000); if(percent >= 90){ clearInterval(intervalId); } return () => { clearInterval(intervalId); }; }, [isModaUploadlOpen,percent]); return ( <div> {isModalOpen ? <div className="pt-[5px]"> <div className="flex"> <div className="ml-[2rem]"> <Switch size="small" defaultChecked onChange={checked => setShowGrid(checked)} /> <span>参考线</span> </div>
<div className="ml-[1rem] "> <Switch size="small" checked={showStandard} onChange={checked => { setShowStandard(checked); if (!checked) { setAngleMarkBackup(showMark); setShowMark(false); } else { setShowMark(angleMarkBackup); } }} /> <span>标准线</span> </div>
<div className="ml-[1rem]"> <Switch size="small" checked={showMark} onChange={checked => { setShowMark(checked); setAngleMarkBackup(checked); }} /> <span>角度线</span> </div> </div> <div className="flex ml-[2rem] mt-[5px]"> <MeasurementCanvas width={800} height={700} logicalExtent={{ minX: -50, maxX: 50, minY: -20, maxY: 60 }} gridStep={1} origin={{ x: 0, y: 20 }} pixelPerMm={8} maxZoom={10} showGrid={showGrid} showBenchmark={showStandard} showAnalysis={showMark} showScale={false} scaleInterval={1} showCoordinates={true} showCalibration={showCalibration} ref={canvasRef} />
{analysisReport && <div className=" h-[550px] ml-[1rem]"> <header className="bg-[#e8f0ff] w-[300px] text-center text-lg font-medium py-2 text-primary border border-[#c1c6d4]"> 分析 </header> <div className="analysis-table"> <table style={{ width: "100%", borderCollapse: "collapse", border: "1px solid #ccc", textAlign: "center", }}> <tbody> <tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}> <td style={{ padding: "8px", border: "1px solid #ccc" }}>W1垂直磨耗</td> <td style={{ padding: "8px", border: "1px solid #ccc" }}>{analysisReport.w1}</td> </tr> <tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}> <td style={{ padding: "8px", border: "1px solid #ccc" }}>轨头宽度</td> <td style={{ padding: "8px", border: "1px solid #ccc" }}> {analysisReport.railHeadWidth} </td> </tr> {analysisReport.angleAnalysisList ? analysisReport.angleAnalysisList.map((item, index) => ( <tr key={index} style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}> <td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.describe}</td> <td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.distance}</td> </tr> )) : <div className="h-[28rem]">加载中……</div> } </tbody> </table> </div> {/* <div className="flex justify-center mt-[1.5rem]" > <Button type="primary" onClick={handleCalibration} className='w-[150px]'> 校准 </Button> </div> */} <div className="flex justify-center mt-[1.5rem]" > <Button type="primary" onClick={handleCancel} className='w-[150px]'> 返回 </Button> </div> </div> } </div> </div> : <> <div className="p-[15px]"> <Button disabled={!selectRows.length} type="primary" onClick={onBatchDel}> 删除 </Button>
<Button className='ml-[2rem]' disabled={!selectRows.length} type="primary" onClick={onUploadData}> 上传 </Button> <Button className='ml-[2rem]' disabled={!selectRows.length} type="primary" onClick={onDownloadData}> 批量导出 </Button>
<Input className='ml-[2rem]' allowClear placeholder="测量名称" onChange={(e)=>setName(e.target.value)} style={{ width: 200 }}/> <Select placeholder="请选择数据来源" className='ml-[2rem] w-[150px]' allowClear onChange={setDataSource} options={SJLY_List.map((item) => ({ label: item.value, value: item.key, }))} ></Select> <Button type="primary" className='ml-[2rem]' onClick={onSearch}>搜索</Button> </div> <div> <Table<DetailTable> style={{height:"70vh"}} locale={{ emptyText: '无数据', }} loading={loading} rowSelection={{ type: selectionType, ...rowSelection }} columns={columns} rowKey="id" dataSource={tableData && tableData.map(item => ({ ...item, key: item.name }))} pagination={false} scroll={{ y: 500 }}
/> <div className="float-right mt-[10px] mr-[1rem]"> <Pagination onChange={onPageChange} current={pageNum} pageSizeOptions={[5,10,20,30]} defaultCurrent={pageNum} defaultPageSize={pageSize} total={total}/> </div>
<Modal title="上传状态" open={isModaUploadlOpen} footer={null} onCancel={onCancelModal}> {/* {idList.length > 0 && <div className='modal_upload'>选择的道岔调查数据:<span style={{color:"green"}}>{idList.length}</span> 个</div>} */} <Progress percent={percent} size="small" /> 共上传: {selectRows.length} 个 {percent === 100 && <div > <div style={{marginTop:"10px"}}> <CheckCircleOutlined style={{color:'#52C41A', fontSize:"20px"}}/> <span style={{paddingLeft:"20px"}}>成功:{successRecordNames.length} 个 {successRecordNames.join(',')}</span> </div> <div style={{marginTop:"10px"}}> <WarningOutlined style={{color:'#ff4d4f', fontSize:"20px"}}/> <span style={{paddingLeft:"20px"}}>失败:{failRecordNames.length} 个 {failRecordNames.join(',')}</span> </div> </div> } <br/> <div style={{marginTop:"20px",marginLeft:"28vw"}}> <Button type="primary" onClick={onCancelModal}>确认</Button> </div> </Modal> </div> </> } </div> ); }
|