|
4 | 4 | * Licensed under the MIT License. See License.txt in the project root for license information. |
5 | 5 | *--------------------------------------------------------------------------------------------*/ |
6 | 6 |
|
7 | | -import React, { useEffect, useState } from 'react'; |
8 | | -import { Drawer, Timeline, Empty } from 'antd'; |
| 7 | +import React, { useContext, useEffect, useRef, useState } from 'react'; |
| 8 | +import { Button, Drawer, Timeline } from 'antd'; |
9 | 9 | import { useParams } from 'react-router-dom'; |
10 | 10 | import { CloseOutlined } from '@ant-design/icons'; |
11 | | -import { getVersion } from '@/shared/http/aipp'; |
12 | | -import { useTranslation } from "react-i18next"; |
13 | | -import tagImg from '@/assets/images/ai/tag.png'; |
| 11 | +import { Message } from '@/shared/utils/message'; |
| 12 | +import { getAppInfo, getVersion, resetApp } from '@/shared/http/aipp'; |
| 13 | +import { useTranslation } from 'react-i18next'; |
| 14 | +import { useAppDispatch } from '@/store/hook'; |
| 15 | +import { setIsReadOnly } from '@/store/common/common'; |
| 16 | +import { RenderContext } from '@/pages/aippIndex/context'; |
| 17 | + |
| 18 | +const PAGE_SIZE = 10; |
14 | 19 |
|
15 | 20 | const TimeLineFc = (props) => { |
16 | | - const { open, setOpen, type = '' } = props; |
| 21 | + const { open, setOpen, type = '', updateAippCallBack } = props; |
17 | 22 | const [timeList, setTimeList] = useState([]); |
18 | 23 | const { tenantId, appId } = useParams(); |
19 | 24 | const { t } = useTranslation(); |
| 25 | + const [selectedAppId, setSelectedAppId] = useState(appId); |
| 26 | + const dispatch = useAppDispatch(); |
| 27 | + const [page, setPage] = useState(1); |
| 28 | + const [loading, setLoading] = useState(false); |
| 29 | + const scrollRef = useRef(); |
| 30 | + const hasMoreRef = useRef<any>(true); |
| 31 | + const currentAppInfo = useRef<any>(null); |
| 32 | + const { renderRef, elsaReadOnlyRef } = useContext(RenderContext); |
20 | 33 |
|
21 | | - useEffect(() => { |
22 | | - open && getVersion(tenantId, appId, type).then(res => { |
| 34 | + const fetchData = async (currentPage: number) => { |
| 35 | + if (!open || loading || !hasMoreRef.current) return; |
| 36 | + setLoading(true); |
| 37 | + try { |
| 38 | + const res = await getVersion(tenantId, appId, type, PAGE_SIZE * (currentPage - 1), PAGE_SIZE); |
23 | 39 | if (res.code === 0) { |
24 | | - setTimeList(res.data); |
| 40 | + const newItems = res.data.results || []; |
| 41 | + setTimeList((prev) => { |
| 42 | + const newIds = new Set(prev.map((item) => item.id)); |
| 43 | + const filteredNewItems = newItems.filter((item) => !newIds.has(item.id)); |
| 44 | + return [...prev, ...filteredNewItems]; |
| 45 | + }); |
| 46 | + if (newItems.length < PAGE_SIZE) { |
| 47 | + hasMoreRef.current = false; |
| 48 | + } else { |
| 49 | + setPage(currentPage + 1); |
| 50 | + } |
25 | 51 | } |
26 | | - }) |
| 52 | + } finally { |
| 53 | + setLoading(false); |
| 54 | + } |
| 55 | + }; |
| 56 | + |
| 57 | + const getCurrentApp = async (tenantId: string, appId: string) => { |
| 58 | + const res: any = await getAppInfo(tenantId, appId); |
| 59 | + if (res.code === 0) { |
| 60 | + currentAppInfo.current = res.data; |
| 61 | + } |
| 62 | + }; |
| 63 | + |
| 64 | + useEffect(() => { |
| 65 | + console.log(open) |
| 66 | + if (open) { |
| 67 | + dispatch(setIsReadOnly(true)); |
| 68 | + setTimeList([]); |
| 69 | + setPage(1); |
| 70 | + hasMoreRef.current = true; |
| 71 | + window.agent?.readOnly(); |
| 72 | + |
| 73 | + Promise.all([ |
| 74 | + getCurrentApp(tenantId, appId), // 刷新当前应用数据 |
| 75 | + fetchData(1) // 加载历史版本 |
| 76 | + ]).catch(console.error); |
| 77 | + } |
27 | 78 | }, [open]); |
28 | | - const descProcess = (str) => { |
29 | | - if (!str || str === 'null') { |
30 | | - return ''; |
| 79 | + |
| 80 | + useEffect(() => { |
| 81 | + const handleScroll = () => { |
| 82 | + const container = scrollRef.current; |
| 83 | + if (!container) return; |
| 84 | + const { scrollTop, scrollHeight, clientHeight } = container; |
| 85 | + if (scrollTop + clientHeight >= scrollHeight - 50) { |
| 86 | + fetchData(page); |
| 87 | + } |
| 88 | + }; |
| 89 | + |
| 90 | + const container = scrollRef.current; |
| 91 | + container?.addEventListener('scroll', handleScroll); |
| 92 | + return () => container?.removeEventListener('scroll', handleScroll); |
| 93 | + }, [page, open, hasMoreRef.current]); |
| 94 | + |
| 95 | + |
| 96 | + const descProcess = (str) => (!str || str === 'null' ? '' : str); |
| 97 | + |
| 98 | + const handleRecover = () => { |
| 99 | + if (appId !== selectedAppId) { |
| 100 | + resetApp(tenantId, appId, selectedAppId, { |
| 101 | + 'Content-Type': 'application/json' |
| 102 | + }).then(res => { |
| 103 | + if (res.code === 0) { |
| 104 | + Message({ type: 'success', content: t('resetSucceed') }); |
| 105 | + currentAppInfo.current = res.data; |
| 106 | + handleClose(); |
| 107 | + } |
| 108 | + }); |
31 | 109 | } |
32 | | - return str; |
| 110 | + }; |
| 111 | + |
| 112 | + const handleItemClick = (timeItem) => { |
| 113 | + setSelectedAppId(timeItem.id); |
| 114 | + updateAippCallBack( |
| 115 | + { |
| 116 | + flowGraph: timeItem.flowGraph, |
| 117 | + configFormProperties: timeItem.configFormProperties |
| 118 | + } |
| 119 | + ); |
| 120 | + renderRef.current = false; |
| 121 | + elsaReadOnlyRef.current = true; |
| 122 | + }; |
| 123 | + |
| 124 | + const handleClose = () => { |
| 125 | + dispatch(setIsReadOnly(false)); |
| 126 | + setSelectedAppId(appId); |
| 127 | + setTimeList([]); |
| 128 | + setPage(1); |
| 129 | + hasMoreRef.current = true; |
| 130 | + updateAippCallBack(currentAppInfo.current); |
| 131 | + renderRef.current = false; |
| 132 | + elsaReadOnlyRef.current = false; |
| 133 | + setOpen(false); |
33 | 134 | } |
34 | | - return <> |
| 135 | + |
| 136 | + useEffect(() => { |
| 137 | + return () => { |
| 138 | + // 组件卸载时自动重置 |
| 139 | + dispatch(setIsReadOnly(false)); |
| 140 | + }; |
| 141 | + }, [dispatch]); |
| 142 | + |
| 143 | + return ( |
35 | 144 | <Drawer |
36 | 145 | title={t('publishHistory')} |
37 | 146 | placement='right' |
38 | 147 | width='420px' |
39 | 148 | closeIcon={false} |
40 | | - onClose={() => setOpen(false)} |
| 149 | + onClose={handleClose} |
41 | 150 | open={open} |
42 | | - footer={null} |
43 | | - extra={ |
44 | | - <CloseOutlined onClick={() => setOpen(false)} /> |
45 | | - }> |
46 | | - <div> |
47 | | - <div style={{ marginBottom: '18px', display: 'flex', alignItems: 'center' }}> |
48 | | - <img src={tagImg} /> |
49 | | - <span style={{ marginLeft: '12px' }}>{t('cannotRevertVersion')}</span> |
| 151 | + mask={false} |
| 152 | + footer={ |
| 153 | + <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', padding: '16px 0' }}> |
| 154 | + <Button onClick={handleClose}>{t('exit')}</Button> |
| 155 | + <Button type="primary" onClick={handleRecover} disabled={appId === selectedAppId}>{t('recover')}</Button> |
50 | 156 | </div> |
51 | | - {timeList.length > 0 ? |
| 157 | + } |
| 158 | + extra={<CloseOutlined onClick={handleClose} />} |
| 159 | + > |
| 160 | + <div ref={scrollRef} style={{ maxHeight: '750px', overflowY: 'auto', paddingRight: '8px' }}> |
| 161 | + {timeList.length > 0 ? ( |
52 | 162 | <Timeline> |
53 | | - { timeList.map(timeItem => ( |
54 | | - <Timeline.Item color='#000000'> |
55 | | - <div className="time-line-inner" style={{ color: 'rgb(77, 77, 77)' }}> |
56 | | - <div style={{ fontWeight: '700' }}>{timeItem.appVersion}</div> |
57 | | - <div style={{ margin: '8px 0' }}>{descProcess(timeItem.publishedDescription)}</div> |
58 | | - <div>{timeItem.publishedBy}</div> |
59 | | - <div>{timeItem.publishedAt}</div> |
60 | | - </div> |
61 | | - </Timeline.Item> |
62 | | - )) } |
63 | | - </Timeline> : |
64 | | - <div style={{ marginTop: '300px' }}><Empty description={t('noData')} /></div> |
65 | | - } |
| 163 | + <Timeline.Item |
| 164 | + color={appId === selectedAppId ? 'blue' : '#000000'} |
| 165 | + key={appId} |
| 166 | + > |
| 167 | + <div |
| 168 | + className="time-line-inner" |
| 169 | + style={{ |
| 170 | + color: appId === selectedAppId ? '#1677ff' : 'rgb(77, 77, 77)', |
| 171 | + backgroundColor: appId === selectedAppId ? '#e6f7ff' : 'transparent', |
| 172 | + borderRadius: '4px', |
| 173 | + padding: '8px', |
| 174 | + cursor: 'pointer', |
| 175 | + }} |
| 176 | + onClick={() => handleItemClick(currentAppInfo.current)} |
| 177 | + > |
| 178 | + <div style={{ fontWeight: '700' }}>{t('currentDraft')}</div> |
| 179 | + </div> |
| 180 | + </Timeline.Item> |
| 181 | + {timeList.map(timeItem => { |
| 182 | + const isSelected = timeItem.id === selectedAppId; |
| 183 | + return ( |
| 184 | + <Timeline.Item |
| 185 | + color={isSelected ? 'blue' : '#000000'} |
| 186 | + key={timeItem.id} |
| 187 | + > |
| 188 | + <div |
| 189 | + className="time-line-inner" |
| 190 | + style={{ |
| 191 | + color: isSelected ? '#1677ff' : 'rgb(77, 77, 77)', |
| 192 | + backgroundColor: isSelected ? '#e6f7ff' : 'transparent', |
| 193 | + borderRadius: '4px', |
| 194 | + padding: '8px', |
| 195 | + cursor: 'pointer', |
| 196 | + }} |
| 197 | + onClick={() => handleItemClick(timeItem)} |
| 198 | + > |
| 199 | + <div style={{ fontWeight: '700' }}>{timeItem.version}</div> |
| 200 | + <div style={{ margin: '8px 0' }}>{descProcess(timeItem.publishedDescription)}</div> |
| 201 | + <div>{timeItem.updateBy}</div> |
| 202 | + <div>{timeItem.updateAt}</div> |
| 203 | + </div> |
| 204 | + </Timeline.Item> |
| 205 | + ); |
| 206 | + })} |
| 207 | + {loading && <div style={{ textAlign: 'center', padding: '12px' }}>{t('loading')}...</div>} |
| 208 | + {!hasMoreRef.current && <div style={{ textAlign: 'center', color: '#888' }}>{t('noMore')}</div>} |
| 209 | + </Timeline> |
| 210 | + ) : null} |
66 | 211 | </div> |
67 | 212 | </Drawer> |
68 | | - </> |
| 213 | + ); |
69 | 214 | }; |
70 | 215 |
|
71 | | - |
72 | 216 | export default TimeLineFc; |
0 commit comments