recordDetailUtils.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. export const formatDuration = (milliseconds, forceHours = false) => {
  2. const totalSeconds = Math.max(0, Math.floor((Number(milliseconds) || 0) / 1000));
  3. const hours = Math.floor(totalSeconds / 3600);
  4. const minutes = Math.floor((totalSeconds % 3600) / 60);
  5. const seconds = totalSeconds % 60;
  6. if (forceHours || hours > 0) {
  7. return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  8. }
  9. return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  10. };
  11. export const formatChineseDate = (value) => {
  12. if (!value) return '—';
  13. const date = new Date(value);
  14. if (Number.isNaN(date.getTime())) return '—';
  15. return new Intl.DateTimeFormat('zh-CN', {
  16. year: 'numeric',
  17. month: 'long',
  18. day: 'numeric'
  19. }).format(date);
  20. };
  21. export const formatChineseTime = (value) => {
  22. if (!value) return '—';
  23. const date = new Date(value);
  24. if (Number.isNaN(date.getTime())) return '—';
  25. return new Intl.DateTimeFormat('zh-CN', {
  26. hour: '2-digit',
  27. minute: '2-digit',
  28. second: '2-digit',
  29. hour12: false
  30. }).format(date);
  31. };
  32. export const formatChineseDateTime = (value) => {
  33. if (!value) return '—';
  34. return `${formatChineseDate(value)} ${formatChineseTime(value)}`;
  35. };
  36. export const isContinuationEvent = (event) => (
  37. event?.eventType === 'MARKER' && String(event?.textContent || '').startsWith('续录时间:')
  38. );
  39. export const eventClientId = (event) => event?.clientId || event?.id;
  40. export const photoAssetClientId = (event) => `${eventClientId(event)}-photo`;
  41. export const findPhotoAsset = (event, assets = []) => {
  42. const expected = photoAssetClientId(event).toLowerCase();
  43. return assets
  44. .filter((asset) => String(asset.kind).toUpperCase() === 'PHOTO')
  45. .find((asset) => String(asset.clientId).toLowerCase() === expected);
  46. };
  47. export const selectLatestAudioAsset = (assets = []) => (
  48. assets
  49. .filter((asset) => String(asset.kind).toUpperCase() === 'AUDIO')
  50. .sort((left, right) => {
  51. const leftTime = new Date(left.createdAt || 0).getTime();
  52. const rightTime = new Date(right.createdAt || 0).getTime();
  53. return rightTime - leftTime;
  54. })[0] || null
  55. );
  56. export const sortEvents = (events = []) => (
  57. [...events].sort((left, right) => {
  58. const delta = Number(left.relativeTimeMs || 0) - Number(right.relativeTimeMs || 0);
  59. if (delta !== 0) return delta;
  60. return String(left.createdAt || left.id).localeCompare(String(right.createdAt || right.id));
  61. })
  62. );
  63. export const groupPhotoEvents = (events = []) => (
  64. sortEvents(events.filter((event) => event.eventType === 'PHOTO'))
  65. .reduce((groups, event) => {
  66. const key = String(event.relativeTimeMs || 0);
  67. const existing = groups.find((group) => group.key === key);
  68. if (existing) existing.events.push(event);
  69. else groups.push({ key, relativeTimeMs: Number(event.relativeTimeMs || 0), events: [event] });
  70. return groups;
  71. }, [])
  72. );
  73. export const buildChronoFeedItems = (events = []) => {
  74. const photoGroups = new Map(groupPhotoEvents(events).map((group) => [group.key, group.events]));
  75. const emittedPhotoTimes = new Set();
  76. return sortEvents(events).flatMap((event) => {
  77. if (event.eventType !== 'PHOTO') return [{ event, photoEvents: [] }];
  78. const key = String(event.relativeTimeMs || 0);
  79. if (emittedPhotoTimes.has(key)) return [];
  80. emittedPhotoTimes.add(key);
  81. return [{ event, photoEvents: photoGroups.get(key) || [event] }];
  82. });
  83. };
  84. export const countSessionEvents = (events = []) => ({
  85. photoCount: events.filter((event) => event.eventType === 'PHOTO').length,
  86. noteCount: events.filter((event) => (
  87. event.eventType === 'NOTE'
  88. || (event.eventType === 'MARKER' && !isContinuationEvent(event))
  89. )).length
  90. });
  91. export const makeLocation = (event) => {
  92. if (event?.latitude == null || event?.longitude == null) return null;
  93. return {
  94. name: event.locationName || event.locationAddress || '所在位置',
  95. address: event.locationAddress || '',
  96. latitude: Number(event.latitude),
  97. longitude: Number(event.longitude)
  98. };
  99. };
  100. export const applyLocation = (event, location) => ({
  101. ...event,
  102. locationName: location?.name || '',
  103. locationAddress: location?.address || '',
  104. latitude: location?.latitude ?? null,
  105. longitude: location?.longitude ?? null
  106. });
  107. export const createEvent = ({
  108. relativeTimeMs,
  109. eventType,
  110. textContent = '',
  111. location = null
  112. }) => {
  113. const id = crypto.randomUUID();
  114. return applyLocation({
  115. id,
  116. clientId: id,
  117. relativeTimeMs: Math.round(relativeTimeMs),
  118. eventType,
  119. textContent,
  120. createdAt: new Date().toISOString()
  121. }, location);
  122. };
  123. export const buildSessionPayload = ({
  124. session,
  125. title = session.title,
  126. events = session.events || [],
  127. durationMs = session.durationMs,
  128. endTime = session.endTime,
  129. deletedEventClientIds = []
  130. }) => ({
  131. clientId: session.clientId || session.id,
  132. title: title.trim(),
  133. startTime: session.startTime,
  134. endTime,
  135. durationMs: Math.round(Number(durationMs) || 0),
  136. baseRevision: Number(session.revision) > 0 ? Number(session.revision) : undefined,
  137. deletedEventClientIds,
  138. events: events.map((event) => ({
  139. clientId: eventClientId(event),
  140. relativeTimeMs: Math.round(Number(event.relativeTimeMs) || 0),
  141. eventType: event.eventType,
  142. textContent: event.textContent || '',
  143. voiceStartOffsetMs: event.voiceStartOffsetMs ?? null,
  144. voiceEndOffsetMs: event.voiceEndOffsetMs ?? null,
  145. locationName: event.locationName || '',
  146. locationAddress: event.locationAddress || '',
  147. latitude: event.latitude ?? null,
  148. longitude: event.longitude ?? null
  149. }))
  150. });
  151. export const sanitizeAudioFileName = (title, fallbackExtension = 'm4a') => {
  152. const clean = String(title || '')
  153. .replace(/[/:\\?%*|"<>]/g, '-')
  154. .replace(/[\u0000-\u001f\u007f-\u009f\r\n]/g, '-')
  155. .replace(/[.\s]+$/g, '')
  156. .trim();
  157. return `${clean || '现场记录'}.${fallbackExtension || 'm4a'}`;
  158. };
  159. export const errorMessage = (error, fallback = '操作失败,请稍后重试。') => (
  160. error?.response?.data?.message
  161. || error?.message
  162. || error?.data?.message
  163. || fallback
  164. );