| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- export const formatDuration = (milliseconds, forceHours = false) => {
- const totalSeconds = Math.max(0, Math.floor((Number(milliseconds) || 0) / 1000));
- const hours = Math.floor(totalSeconds / 3600);
- const minutes = Math.floor((totalSeconds % 3600) / 60);
- const seconds = totalSeconds % 60;
- if (forceHours || hours > 0) {
- return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
- }
- return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
- };
- export const formatChineseDate = (value) => {
- if (!value) return '—';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return '—';
- return new Intl.DateTimeFormat('zh-CN', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- }).format(date);
- };
- export const formatChineseTime = (value) => {
- if (!value) return '—';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return '—';
- return new Intl.DateTimeFormat('zh-CN', {
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false
- }).format(date);
- };
- export const formatChineseDateTime = (value) => {
- if (!value) return '—';
- return `${formatChineseDate(value)} ${formatChineseTime(value)}`;
- };
- export const isContinuationEvent = (event) => (
- event?.eventType === 'MARKER' && String(event?.textContent || '').startsWith('续录时间:')
- );
- export const eventClientId = (event) => event?.clientId || event?.id;
- export const photoAssetClientId = (event) => `${eventClientId(event)}-photo`;
- export const findPhotoAsset = (event, assets = []) => {
- const expected = photoAssetClientId(event).toLowerCase();
- return assets
- .filter((asset) => String(asset.kind).toUpperCase() === 'PHOTO')
- .find((asset) => String(asset.clientId).toLowerCase() === expected);
- };
- export const selectLatestAudioAsset = (assets = []) => (
- assets
- .filter((asset) => String(asset.kind).toUpperCase() === 'AUDIO')
- .sort((left, right) => {
- const leftTime = new Date(left.createdAt || 0).getTime();
- const rightTime = new Date(right.createdAt || 0).getTime();
- return rightTime - leftTime;
- })[0] || null
- );
- export const sortEvents = (events = []) => (
- [...events].sort((left, right) => {
- const delta = Number(left.relativeTimeMs || 0) - Number(right.relativeTimeMs || 0);
- if (delta !== 0) return delta;
- return String(left.createdAt || left.id).localeCompare(String(right.createdAt || right.id));
- })
- );
- export const groupPhotoEvents = (events = []) => (
- sortEvents(events.filter((event) => event.eventType === 'PHOTO'))
- .reduce((groups, event) => {
- const key = String(event.relativeTimeMs || 0);
- const existing = groups.find((group) => group.key === key);
- if (existing) existing.events.push(event);
- else groups.push({ key, relativeTimeMs: Number(event.relativeTimeMs || 0), events: [event] });
- return groups;
- }, [])
- );
- export const buildChronoFeedItems = (events = []) => {
- const photoGroups = new Map(groupPhotoEvents(events).map((group) => [group.key, group.events]));
- const emittedPhotoTimes = new Set();
- return sortEvents(events).flatMap((event) => {
- if (event.eventType !== 'PHOTO') return [{ event, photoEvents: [] }];
- const key = String(event.relativeTimeMs || 0);
- if (emittedPhotoTimes.has(key)) return [];
- emittedPhotoTimes.add(key);
- return [{ event, photoEvents: photoGroups.get(key) || [event] }];
- });
- };
- export const countSessionEvents = (events = []) => ({
- photoCount: events.filter((event) => event.eventType === 'PHOTO').length,
- noteCount: events.filter((event) => (
- event.eventType === 'NOTE'
- || (event.eventType === 'MARKER' && !isContinuationEvent(event))
- )).length
- });
- export const makeLocation = (event) => {
- if (event?.latitude == null || event?.longitude == null) return null;
- return {
- name: event.locationName || event.locationAddress || '所在位置',
- address: event.locationAddress || '',
- latitude: Number(event.latitude),
- longitude: Number(event.longitude)
- };
- };
- export const applyLocation = (event, location) => ({
- ...event,
- locationName: location?.name || '',
- locationAddress: location?.address || '',
- latitude: location?.latitude ?? null,
- longitude: location?.longitude ?? null
- });
- export const createEvent = ({
- relativeTimeMs,
- eventType,
- textContent = '',
- location = null
- }) => {
- const id = crypto.randomUUID();
- return applyLocation({
- id,
- clientId: id,
- relativeTimeMs: Math.round(relativeTimeMs),
- eventType,
- textContent,
- createdAt: new Date().toISOString()
- }, location);
- };
- export const buildSessionPayload = ({
- session,
- title = session.title,
- events = session.events || [],
- durationMs = session.durationMs,
- endTime = session.endTime,
- deletedEventClientIds = []
- }) => ({
- clientId: session.clientId || session.id,
- title: title.trim(),
- startTime: session.startTime,
- endTime,
- durationMs: Math.round(Number(durationMs) || 0),
- baseRevision: Number(session.revision) > 0 ? Number(session.revision) : undefined,
- deletedEventClientIds,
- events: events.map((event) => ({
- clientId: eventClientId(event),
- relativeTimeMs: Math.round(Number(event.relativeTimeMs) || 0),
- eventType: event.eventType,
- textContent: event.textContent || '',
- voiceStartOffsetMs: event.voiceStartOffsetMs ?? null,
- voiceEndOffsetMs: event.voiceEndOffsetMs ?? null,
- locationName: event.locationName || '',
- locationAddress: event.locationAddress || '',
- latitude: event.latitude ?? null,
- longitude: event.longitude ?? null
- }))
- });
- export const sanitizeAudioFileName = (title, fallbackExtension = 'm4a') => {
- const clean = String(title || '')
- .replace(/[/:\\?%*|"<>]/g, '-')
- .replace(/[\u0000-\u001f\u007f-\u009f\r\n]/g, '-')
- .replace(/[.\s]+$/g, '')
- .trim();
- return `${clean || '现场记录'}.${fallbackExtension || 'm4a'}`;
- };
- export const errorMessage = (error, fallback = '操作失败,请稍后重试。') => (
- error?.response?.data?.message
- || error?.message
- || error?.data?.message
- || fallback
- );
|