version-history-modal.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. 'use client'
  2. import { useEffect, useState } from 'react'
  3. import { RiCloseLine, RiHistoryLine } from '@remixicon/react'
  4. import Modal from '@/app/components/base/modal'
  5. import Button from '@/app/components/base/button'
  6. import Loading from '@/app/components/base/loading'
  7. import { fetchSegmentVersions, type SegmentVersion } from '@/service/datasets'
  8. import cn from '@/utils/classnames'
  9. type VersionHistoryModalProps = {
  10. datasetId: string
  11. documentId: string
  12. segmentId: string
  13. isOpen: boolean
  14. onClose: () => void
  15. }
  16. export default function VersionHistoryModal({
  17. datasetId,
  18. documentId,
  19. segmentId,
  20. isOpen,
  21. onClose,
  22. }: VersionHistoryModalProps) {
  23. const [loading, setLoading] = useState(true)
  24. const [versions, setVersions] = useState<SegmentVersion[]>([])
  25. const [total, setTotal] = useState(0)
  26. const [selectedVersion, setSelectedVersion] = useState<SegmentVersion | null>(null)
  27. useEffect(() => {
  28. if (isOpen)
  29. loadVersions()
  30. }, [isOpen])
  31. const loadVersions = async () => {
  32. setLoading(true)
  33. try {
  34. const result = await fetchSegmentVersions(datasetId, documentId, segmentId, {
  35. page: 1,
  36. per_page: 20,
  37. include_content: false,
  38. })
  39. setVersions(result.versions)
  40. setTotal(result.total)
  41. }
  42. catch (error) {
  43. console.error('Failed to load versions:', error)
  44. }
  45. finally {
  46. setLoading(false)
  47. }
  48. }
  49. const loadVersionDetail = async (version: SegmentVersion) => {
  50. try {
  51. const detail = await fetchSegmentVersions(datasetId, documentId, segmentId, {
  52. page: 1,
  53. per_page: 1,
  54. include_content: true,
  55. })
  56. const fullVersion = detail.versions.find(v => v.version_number === version.version_number)
  57. if (fullVersion)
  58. setSelectedVersion(fullVersion)
  59. }
  60. catch (error) {
  61. console.error('Failed to load version detail:', error)
  62. }
  63. }
  64. const getChangeTypeColor = (changeType: string) => {
  65. switch (changeType) {
  66. case 'create':
  67. return 'bg-green-100 text-green-700 border-green-200'
  68. case 'update':
  69. return 'bg-blue-100 text-blue-700 border-blue-200'
  70. case 'delete':
  71. return 'bg-red-100 text-red-700 border-red-200'
  72. default:
  73. return 'bg-gray-100 text-gray-700 border-gray-200'
  74. }
  75. }
  76. const formatDateTime = (dateStr: string) => {
  77. const date = new Date(dateStr)
  78. return date.toLocaleString(undefined, {
  79. month: 'short',
  80. day: 'numeric',
  81. hour: '2-digit',
  82. minute: '2-digit',
  83. })
  84. }
  85. return (
  86. <>
  87. <Modal
  88. isShow={isOpen}
  89. onClose={onClose}
  90. title="版本历史"
  91. className="!max-w-3xl"
  92. >
  93. <div className="h-[500px] flex flex-col">
  94. {total > 0 && (
  95. <div className="px-6 py-3 bg-gray-50 border-b text-sm text-gray-600">
  96. 共 {total} 个版本
  97. </div>
  98. )}
  99. <div className="flex-1 overflow-y-auto px-6 py-4">
  100. {loading
  101. ? (
  102. <div className="flex items-center justify-center h-40">
  103. <Loading />
  104. </div>
  105. )
  106. : versions.length === 0
  107. ? (
  108. <div className="flex flex-col items-center justify-center h-40 text-gray-500">
  109. <RiHistoryLine className="w-12 h-12 mb-2 text-gray-400" />
  110. <p>暂无版本历史</p>
  111. </div>
  112. )
  113. : (
  114. <div className="space-y-3">
  115. {versions.map((version, index) => {
  116. const isLatest = index === 0
  117. return (
  118. <div
  119. key={version.version_number}
  120. className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
  121. >
  122. <div className="flex items-start justify-between gap-4">
  123. <div className="flex-1 min-w-0">
  124. <div className="flex items-center gap-2 mb-2">
  125. <span className="font-semibold text-gray-900">
  126. 版本 {version.version_number}
  127. </span>
  128. <span className={cn(
  129. 'px-2 py-0.5 text-xs font-medium rounded-full border',
  130. getChangeTypeColor(version.change_type),
  131. )}>
  132. {version.change_type === 'create' && '创建'}
  133. {version.change_type === 'update' && '更新'}
  134. {version.change_type === 'delete' && '删除'}
  135. {version.change_type === 'rollback' && '回滚'}
  136. </span>
  137. {isLatest && (
  138. <span className="px-2 py-0.5 text-xs font-medium rounded-full bg-primary-50 text-primary-700 border border-primary-200">
  139. 当前版本
  140. </span>
  141. )}
  142. </div>
  143. <p className="text-sm text-gray-700 mb-2">
  144. {version.change_summary}
  145. </p>
  146. {version.changed_fields && version.changed_fields.length > 0 && (
  147. <div className="flex items-center gap-2 mb-2">
  148. <span className="text-xs text-gray-500">
  149. 修改字段:
  150. </span>
  151. <div className="flex flex-wrap gap-1">
  152. {version.changed_fields.map(field => (
  153. <span
  154. key={field}
  155. className="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded"
  156. >
  157. {field}
  158. </span>
  159. ))}
  160. </div>
  161. </div>
  162. )}
  163. <div className="flex items-center gap-4 text-xs text-gray-500">
  164. <span>
  165. 创建者: {version.created_by_name}
  166. </span>
  167. <span>•</span>
  168. <span>{formatDateTime(version.created_at)}</span>
  169. </div>
  170. </div>
  171. <div className="flex-shrink-0">
  172. <Button
  173. variant="ghost"
  174. size="small"
  175. onClick={() => loadVersionDetail(version)}
  176. >
  177. 查看
  178. </Button>
  179. </div>
  180. </div>
  181. </div>
  182. )
  183. })}
  184. </div>
  185. )}
  186. </div>
  187. </div>
  188. </Modal>
  189. {selectedVersion && (
  190. <Modal
  191. isShow={!!selectedVersion}
  192. onClose={() => setSelectedVersion(null)}
  193. title={`版本 ${selectedVersion.version_number}`}
  194. className="!max-w-2xl"
  195. >
  196. <div className="max-h-[60vh] overflow-y-auto space-y-4">
  197. <div>
  198. <label className="block text-sm font-medium text-gray-700 mb-2">
  199. 内容
  200. </label>
  201. <div className="p-4 bg-gray-50 rounded-lg border border-gray-200 max-h-60 overflow-y-auto">
  202. <pre className="whitespace-pre-wrap text-sm text-gray-900 font-sans">
  203. {selectedVersion.content}
  204. </pre>
  205. </div>
  206. </div>
  207. {selectedVersion.answer && (
  208. <div>
  209. <label className="block text-sm font-medium text-gray-700 mb-2">
  210. 答案
  211. </label>
  212. <div className="p-4 bg-blue-50 rounded-lg border border-blue-200 max-h-40 overflow-y-auto">
  213. <pre className="whitespace-pre-wrap text-sm text-gray-900 font-sans">
  214. {selectedVersion.answer}
  215. </pre>
  216. </div>
  217. </div>
  218. )}
  219. </div>
  220. </Modal>
  221. )}
  222. </>
  223. )
  224. }