You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

651 lines
18 KiB

2 months ago
5 months ago
5 months ago
5 months ago
5 months ago
2 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
3 months ago
5 months ago
5 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
  1. import {getDetailList, delDetail, getDetail, getPointByUuid, getPointsById, getAlignPointById} from '../../../services/measure/analysis'
  2. import { useState, useEffect, useRef } from 'react'
  3. import {message, Button, type TableColumnsType, type TableProps, Modal, Table, Pagination, Input, Select, Switch, Progress, Flex, Spin } from 'antd';
  4. import type { AnalysisReport, DetailTable, SearchParams } from "../../../services/measure/type";
  5. import { ExclamationCircleFilled, CheckCircleOutlined, WarningOutlined } from '@ant-design/icons';
  6. import { AnalysisData, BenchmarkShape, MeasurementCanvasRef } from './konva/MeasurementCanvas';
  7. import MeasurementCanvas from "./konva/MeasurementCanvas";
  8. import {
  9. dictionaryListService,
  10. kljUpload
  11. } from "../../../services/ktj/org";
  12. import {
  13. getReport
  14. } from "../../../services/measure/analysis";
  15. import { getBaseRecordPointSetByCode } from "../../../services/track/trackShape"
  16. import { extraDescType, KTJ_BASE_TYPE } from '../../../services/ktjTypes';
  17. import { GX_CODE } from '../../../constant';
  18. import { exportFile, padNumber } from '../../../utils';
  19. export default function MeasureDetail() {
  20. useEffect(()=>{
  21. queryDictionaryList()
  22. const params = {
  23. pageSize,
  24. pageNum,
  25. }
  26. getDetailDataList(params)
  27. }, [])
  28. const [selectRows, setSelectedRow] = useState<DetailTable[]>([])
  29. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  30. const rowSelection = {
  31. selectedRowKeys,
  32. onChange: (selectedRowKeys: React.Key[], selectedRows: DetailTable[]) => {
  33. console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
  34. setSelectedRow(selectedRows)
  35. setSelectedRowKeys(selectedRowKeys)
  36. },
  37. getCheckboxProps: (record: DetailTable) => ({
  38. name: record.name,
  39. }),
  40. };
  41. const columns: TableColumnsType<DetailTable> = [
  42. {
  43. title: '序号',
  44. dataIndex: 'seq',
  45. render:(_, record, index)=>{
  46. return index + 1
  47. }
  48. },
  49. {
  50. title: '测量名称',
  51. dataIndex: 'name',
  52. },
  53. {
  54. title: '数据来源',
  55. dataIndex: 'dataSource',
  56. render:(_, record)=>{
  57. const dataSource = record.dataSource
  58. return dataSource === 'DCDC' ? '道岔调查' : '线路调查'
  59. }
  60. },
  61. {
  62. title: '轨型',
  63. dataIndex: 'railSize',
  64. },
  65. {
  66. title: '当天测量序号',
  67. dataIndex: 'todayNumber',
  68. render: (_, record) => (
  69. padNumber(record.todayNumber, 4)
  70. )
  71. },
  72. {
  73. title: '创建者',
  74. dataIndex: 'operator',
  75. },
  76. {
  77. title: '时间',
  78. dataIndex: 'createTime',
  79. },
  80. {
  81. title: '操作',
  82. dataIndex: 'op',
  83. width:180,
  84. align:'center',
  85. render:(_, record)=>{
  86. return <div>
  87. <Button type="link" onClick={()=>onShowDetail(record)}></Button>
  88. <Button type="link" onClick={()=>onDownloadRecord(record)}></Button>
  89. </div>
  90. }
  91. },
  92. ];
  93. const [tableData, setTableData] = useState<DetailTable[]>([])
  94. const [total, setTotal] = useState(0)
  95. const getDetailDataList = (params:SearchParams) => {
  96. setLoading(true)
  97. getDetailList(params).then(res => {
  98. if(res.success){
  99. setLoading(false)
  100. //@ts-ignore
  101. setTableData(res.data.list)
  102. setTotal(res.data.total)
  103. }else{
  104. }
  105. }).catch(e=>{
  106. }).finally(()=>{
  107. setLoading(false)
  108. })
  109. }
  110. const [selectionType, setSelectionType] = useState<'checkbox'>('checkbox');
  111. const { confirm } = Modal;
  112. const onBatchDel = () => {
  113. confirm({
  114. title: '提示',
  115. icon: <ExclamationCircleFilled />,
  116. content: '请确认是否删除选中的数据',
  117. okText:'确认',
  118. cancelText:'取消',
  119. onOk() {
  120. onHandelDelData()
  121. },
  122. onCancel() {
  123. console.log('Cancel');
  124. },
  125. });
  126. }
  127. let [idList, setIdList] = useState<number[]>([]);
  128. let [isModaUploadlOpen, setIsModaUploadlOpen] = useState(false)
  129. const onUploadData = () => {
  130. setIdList([])
  131. let list = [...selectRows]
  132. let Ids:number[] = []
  133. list.map(item => {
  134. Ids.push(item.id)
  135. })
  136. setIdList(Ids)
  137. handleUpload(Ids)
  138. setIsModaUploadlOpen(true)
  139. }
  140. const onDownloadData = async () => {
  141. // 1. 准备 id 列表
  142. const Ids = selectRows.map(r => r.id);
  143. // 2. 发起请求
  144. const response = await fetch(`/api/measurement-data/downloads-zip/${Ids.join(',')}`);
  145. // 3. 拿到 Blob
  146. const blob = await response.blob();
  147. // 4. 解析后端文件名
  148. const contentDisp = response.headers.get('content-disposition');
  149. const filename = parseFilename(contentDisp) || 'download.zip';
  150. // 5. 触发下载
  151. exportFile(blob, filename);
  152. };
  153. const [percent, setPercent] = useState(0)
  154. const [failRecordNames, setFailRecordName] = useState<string[]>([])
  155. const [successRecordNames, setSuccessRecordName] = useState<string[]>([])
  156. const handleUpload = (Ids:number[]) => {
  157. if(Ids && Ids.length){
  158. kljUpload({
  159. ids:Ids
  160. }).then(res => {
  161. if(res.status !== 0){
  162. message.error('道岔数据上传失败')
  163. }else{
  164. message.success('上传成功')
  165. if(res.data.successList && res.data.successList.length){
  166. let list:string[] = []
  167. res.data.successList.map((item:string) => {
  168. let recordName = item.split(":")[0]
  169. list.push(recordName)
  170. })
  171. setSuccessRecordName(list)
  172. }
  173. if(res.data.failList && res.data.failList.length){
  174. let list:string[] = []
  175. res.data.failList.map((item:string) => {
  176. let recordName = item.split(":")[0]
  177. list.push(recordName)
  178. })
  179. setFailRecordName(list)
  180. }
  181. setPercent(100)
  182. }
  183. }).catch(e=>{
  184. })
  185. }
  186. }
  187. //关闭上传
  188. const handleCloseUpload = () => {
  189. }
  190. const [currentRecord, setCurrentRecord] = useState<Partial<DetailTable>>({})
  191. const onShowDetail = async (item:DetailTable)=> {
  192. //获取基线
  193. setIsModalOpen(true)
  194. setCurrentRecord(item)
  195. setshowCalibration(false)
  196. let res = await getBaseRecordPointSetByCode(item.railSize)
  197. if (res.success && res.data && res.data.points) {
  198. const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[];
  199. if(!benchmarkShapes || !benchmarkShapes.length){
  200. message.error('选择的测量记录没有数据')
  201. return;
  202. }
  203. setTimeout(()=>{
  204. if (canvasRef.current) {
  205. console.log("解析后的基础图形数据:", benchmarkShapes);
  206. canvasRef.current.setBenchmarkData(benchmarkShapes);
  207. }
  208. },100)
  209. }
  210. let resData = await getDetail({id:item.id})
  211. if(resData){
  212. // navigate(`/measure/detail/${item.id}`)
  213. //@ts-ignore
  214. // getRecordByUuid(resData.data.uuid)
  215. //@ts-ignore
  216. onDetaiResult(resData.data.uuid)
  217. }
  218. //获取测量的坐标点
  219. getMeasurePoints(item)
  220. }
  221. function parseFilename(disposition: string | null): string | null {
  222. if (!disposition) return null;
  223. // 匹配 filename*=UTF-8''xxx 或 filename=xxx(带不带引号都能捕获)
  224. const match = disposition.match(/filename\*?=(?:UTF-8'')?["']?([^"';]+)["']?/i);
  225. return match ? decodeURIComponent(match[1]) : null;
  226. }
  227. //导出
  228. const onDownloadRecord = async (item: DetailTable) => {
  229. // 1. 发请求
  230. const response = await fetch(`/api/measurement-data/download/${item.id}`);
  231. // 2. 拿到 Blob
  232. const blob = await response.blob();
  233. // 3. 解析头里的文件名
  234. const contentDisp = response.headers.get('content-disposition');
  235. // fallback 到 item.name + .txt
  236. const filename = parseFilename(contentDisp) || `${item.name}.txt`;
  237. // 4. 触发下载
  238. exportFile(blob, filename);
  239. };
  240. const getMeasurePoints = (recordItem:DetailTable) => {
  241. getPointsById({id:recordItem.id}).then(res=>{
  242. if (canvasRef.current) {
  243. // canvasRef.current.setMeasurementDataLeft(res.data.leftPoints)
  244. // canvasRef.current.setMeasurementDataRight(res.data.rightPoints)
  245. setshowCalibration(true)
  246. canvasRef.current?.setMeasurementCalibrationData(res.data.alignPoints)
  247. }
  248. })
  249. }
  250. //初始化科天健机构数据
  251. let [SJLY_List, setSJLYList] = useState<KTJ_BASE_TYPE[]>([]);
  252. const queryDictionaryList = () => {
  253. dictionaryListService().then((res) => {
  254. if (res && res.data) {
  255. const SJLY:KTJ_BASE_TYPE[] = res.data.SJLY
  256. if(SJLY && SJLY.length){
  257. let sjlylist = SJLY.filter(item => item.key === 'XLDC' || item.key === 'DCDC')
  258. setSJLYList(sjlylist);
  259. }
  260. }
  261. });
  262. };
  263. const onCancelModal = () => {
  264. setIsModaUploadlOpen(false)
  265. setPercent(0)
  266. setFailRecordName([])
  267. setSuccessRecordName([])
  268. }
  269. //法线
  270. const [analysisReport, setAnalysisReport] = useState<AnalysisReport>();
  271. const onDetaiResult = (uuid:string) => {
  272. const gxValue= currentRecord.railSize || GX_CODE
  273. const params = {
  274. uuid,
  275. code:gxValue
  276. }
  277. getReport(params).then(res=> {
  278. if (res.success) {
  279. const report: AnalysisReport = res.data;
  280. console.log(report);
  281. // 更新 canvas 的分析数据
  282. if (report && report.angleAnalysisList) {
  283. // 先过滤掉 distance 为 null 的数据
  284. const validItems = report.angleAnalysisList.filter(item => item.distance !== null);
  285. const analysisData: AnalysisData[] = validItems.map(item => ({
  286. pointA: { x: parseFloat(item.pointA ? item.pointA.x : '0'), y: parseFloat(item.pointA ? item.pointA.y : '0') },
  287. pointB: { x: parseFloat(item.pointB ? item.pointB.x : '0'), y: parseFloat(item.pointB ? item.pointB.y : '0') },
  288. base: { x: parseFloat(item.pointA ? item.pointA.x : '0'), y: parseFloat(item.pointA ? item.pointA.y : '0') },
  289. measure: { x: parseFloat(item.pointB ? item.pointB.x : '0'), y: parseFloat(item.pointB ? item.pointB.y : '0') },
  290. distance: parseFloat(item.distance),
  291. describe: item.describe,
  292. }));
  293. canvasRef.current?.setAnalysisData(analysisData);
  294. }
  295. setAnalysisReport(report);
  296. } else {
  297. message.error("分析报告请求失败: " + res.data.info);
  298. }
  299. })
  300. }
  301. const getRecordByUuid = (uuid:string) => {
  302. getPointByUuid({uuid}).then(res=>{
  303. if(res.data && res.data.points){
  304. canvasRef.current?.setMeasurementData(res.data.points);
  305. }
  306. })
  307. }
  308. type DelParams = {
  309. ids: string | number;
  310. }
  311. const onHandelDelData = () =>{
  312. let list = [...selectRows]
  313. const ids = list.map(item => item.id)
  314. if(!ids || !ids.length){
  315. return;
  316. }
  317. const params = {
  318. ids:ids.join(',')
  319. }
  320. doDel(params)
  321. }
  322. const doDel = (params:DelParams) => {
  323. delDetail(params).then(res => {
  324. if(res.success){
  325. message.success('删除成功')
  326. const params = {
  327. pageSize,
  328. pageNum,
  329. }
  330. getDetailDataList(params)
  331. setSelectedRowKeys([])
  332. setSelectedRow([])
  333. }
  334. }).catch(e=> {
  335. })
  336. }
  337. const [pageNum, setPageNum] = useState(1)
  338. const [pageSize, setPageSize] = useState(8)
  339. const onPageChange = (pageNumValue:number, pageSizeValue:number) => {
  340. setPageNum(pageNumValue)
  341. setPageSize(pageSizeValue)
  342. searchParams = {
  343. ...searchParams,
  344. pageSize:pageSizeValue,
  345. pageNum:pageNumValue,
  346. }
  347. getDetailDataList(searchParams)
  348. }
  349. const [name, setName] = useState<string>()
  350. const [dataSource, setDataSource] = useState<string>()
  351. let searchParams:SearchParams = {
  352. pageNum,
  353. pageSize,
  354. }
  355. const onSearch = ()=> {
  356. setPageNum(1)
  357. setPageSize(5)
  358. searchParams = {
  359. pageSize,
  360. pageNum: 1,
  361. name,
  362. dataSource
  363. }
  364. getDetailDataList(searchParams)
  365. }
  366. let [loading, setLoading] = useState(false)
  367. let [isModalOpen, setIsModalOpen] = useState(false)
  368. const canvasRef = useRef<MeasurementCanvasRef>(null);
  369. const [showGrid, setShowGrid] = useState(true);
  370. const [angleMarkBackup, setAngleMarkBackup] = useState(true);
  371. const [showStandard, setShowStandard] = useState(true);
  372. const [showMark, setShowMark] = useState(true);
  373. const handleCancel = () => {
  374. canvasRef.current?.setAnalysisData([]);
  375. canvasRef.current?.setMeasurementData([]);
  376. canvasRef.current?.setBenchmarkData([])
  377. //@ts-ignore
  378. setAnalysisReport({})
  379. setIsModalOpen(false)
  380. }
  381. //校准
  382. const [showCalibration, setshowCalibration] = useState(false)
  383. const [caloading, setCaLoading] = useState(false)
  384. const handleCalibration = () => {
  385. setCaLoading(true)
  386. if(currentRecord && currentRecord.id){
  387. getAlignPointById({id:currentRecord.id}).then(res=>{
  388. if(res.success){
  389. setshowCalibration(true)
  390. canvasRef.current?.setMeasurementCalibrationData(res.data)
  391. }else{
  392. message.error('校准失败!')
  393. }
  394. setCaLoading(false)
  395. }).catch(e=>{
  396. message.error('校准失败!')
  397. })
  398. }
  399. }
  400. useEffect(() => {
  401. const intervalId = setInterval(() => {
  402. if (percent < 100) {
  403. const randomIncrement = Math.floor(Math.random() * 3) + 1;
  404. const newProgress = Math.min(percent + randomIncrement, 100);
  405. setPercent(newProgress);
  406. } else {
  407. clearInterval(intervalId);
  408. }
  409. }, 1000);
  410. if(percent >= 90){
  411. clearInterval(intervalId);
  412. }
  413. return () => {
  414. clearInterval(intervalId);
  415. };
  416. }, [isModaUploadlOpen,percent]);
  417. return (
  418. <div>
  419. {isModalOpen ?
  420. <div className="pt-[5px]">
  421. <div className="flex">
  422. <div className="ml-[2rem]">
  423. <Switch size="small" defaultChecked onChange={checked => setShowGrid(checked)} />
  424. <span>线</span>
  425. </div>
  426. <div className="ml-[1rem] ">
  427. <Switch
  428. size="small"
  429. checked={showStandard}
  430. onChange={checked => {
  431. setShowStandard(checked);
  432. if (!checked) {
  433. setAngleMarkBackup(showMark);
  434. setShowMark(false);
  435. } else {
  436. setShowMark(angleMarkBackup);
  437. }
  438. }}
  439. />
  440. <span>线</span>
  441. </div>
  442. <div className="ml-[1rem]">
  443. <Switch
  444. size="small"
  445. checked={showMark}
  446. onChange={checked => {
  447. setShowMark(checked);
  448. setAngleMarkBackup(checked);
  449. }}
  450. />
  451. <span>线</span>
  452. </div>
  453. </div>
  454. <div className="flex ml-[2rem] mt-[5px]">
  455. <MeasurementCanvas
  456. width={800}
  457. height={700}
  458. logicalExtent={{ minX: -50, maxX: 50, minY: -20, maxY: 60 }}
  459. gridStep={1}
  460. origin={{ x: 0, y: 20 }}
  461. pixelPerMm={8}
  462. maxZoom={10}
  463. showGrid={showGrid}
  464. showBenchmark={showStandard}
  465. showAnalysis={showMark}
  466. showScale={false}
  467. scaleInterval={1}
  468. showCoordinates={true}
  469. showCalibration={showCalibration}
  470. ref={canvasRef}
  471. />
  472. {analysisReport &&
  473. <div className=" h-[550px] ml-[1rem]">
  474. <header className="bg-[#e8f0ff] w-[300px] text-center text-lg font-medium py-2 text-primary border border-[#c1c6d4]">
  475. </header>
  476. <div className="analysis-table">
  477. <table
  478. style={{
  479. width: "100%",
  480. borderCollapse: "collapse",
  481. border: "1px solid #ccc",
  482. textAlign: "center",
  483. }}>
  484. <tbody>
  485. <tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
  486. <td style={{ padding: "8px", border: "1px solid #ccc" }}>W1垂直磨耗</td>
  487. <td style={{ padding: "8px", border: "1px solid #ccc" }}>{analysisReport.w1}</td>
  488. </tr>
  489. <tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
  490. <td style={{ padding: "8px", border: "1px solid #ccc" }}></td>
  491. <td style={{ padding: "8px", border: "1px solid #ccc" }}>
  492. {analysisReport.railHeadWidth}
  493. </td>
  494. </tr>
  495. {analysisReport.angleAnalysisList ? analysisReport.angleAnalysisList.map((item, index) => (
  496. <tr key={index} style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
  497. <td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.describe}</td>
  498. <td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.distance}</td>
  499. </tr>
  500. ))
  501. :
  502. <div className="h-[28rem]"></div>
  503. }
  504. </tbody>
  505. </table>
  506. </div>
  507. {/* <div className="flex justify-center mt-[1.5rem]" >
  508. <Button type="primary" onClick={handleCalibration} className='w-[150px]'>
  509. </Button>
  510. </div> */}
  511. <div className="flex justify-center mt-[1.5rem]" >
  512. <Button type="primary" onClick={handleCancel} className='w-[150px]'>
  513. </Button>
  514. </div>
  515. </div>
  516. }
  517. </div>
  518. </div>
  519. :
  520. <>
  521. <div className="p-[15px]">
  522. <Button
  523. disabled={!selectRows.length}
  524. type="primary"
  525. onClick={onBatchDel}>
  526. </Button>
  527. <Button
  528. className='ml-[2rem]'
  529. disabled={!selectRows.length}
  530. type="primary"
  531. onClick={onUploadData}>
  532. </Button>
  533. <Button
  534. className='ml-[2rem]'
  535. disabled={!selectRows.length}
  536. type="primary"
  537. onClick={onDownloadData}>
  538. </Button>
  539. <Input className='ml-[2rem]' allowClear placeholder="测量名称" onChange={(e)=>setName(e.target.value)} style={{ width: 200 }}/>
  540. <Select
  541. placeholder="请选择数据来源"
  542. className='ml-[2rem] w-[150px]'
  543. allowClear
  544. onChange={setDataSource}
  545. options={SJLY_List.map((item) => ({
  546. label: item.value,
  547. value: item.key,
  548. }))}
  549. ></Select>
  550. <Button type="primary" className='ml-[2rem]' onClick={onSearch}></Button>
  551. </div>
  552. <div>
  553. <Table<DetailTable>
  554. style={{height:"70vh"}}
  555. locale={{
  556. emptyText: '无数据',
  557. }}
  558. loading={loading}
  559. rowSelection={{ type: selectionType, ...rowSelection }}
  560. columns={columns}
  561. rowKey="id"
  562. dataSource={tableData && tableData.map(item => ({ ...item, key: item.name }))}
  563. pagination={false}
  564. scroll={{ y: 500 }}
  565. />
  566. <div className="float-right mt-[10px] mr-[1rem]">
  567. <Pagination onChange={onPageChange} current={pageNum} pageSizeOptions={[5,10,20,30]} defaultCurrent={pageNum} defaultPageSize={pageSize} total={total}/>
  568. </div>
  569. <Modal title="上传状态" open={isModaUploadlOpen} footer={null} onCancel={onCancelModal}>
  570. {/* {idList.length > 0 && <div className='modal_upload'>选择的道岔调查数据:<span style={{color:"green"}}>{idList.length}</span>&nbsp;&nbsp;个</div>} */}
  571. <Progress percent={percent} size="small" />
  572. {selectRows.length}
  573. {percent === 100 &&
  574. <div >
  575. <div style={{marginTop:"10px"}}>
  576. <CheckCircleOutlined style={{color:'#52C41A', fontSize:"20px"}}/>
  577. <span style={{paddingLeft:"20px"}}>{successRecordNames.length} {successRecordNames.join(',')}</span>
  578. </div>
  579. <div style={{marginTop:"10px"}}>
  580. <WarningOutlined style={{color:'#ff4d4f', fontSize:"20px"}}/>
  581. <span style={{paddingLeft:"20px"}}>{failRecordNames.length} {failRecordNames.join(',')}</span>
  582. </div>
  583. </div>
  584. }
  585. <br/>
  586. <div style={{marginTop:"20px",marginLeft:"28vw"}}>
  587. <Button type="primary" onClick={onCancelModal}></Button>
  588. </div>
  589. </Modal>
  590. </div>
  591. </>
  592. }
  593. </div>
  594. );
  595. }