refactor(date): consolidate duration formatting into three date helpers
Five hand-rolled formatDuration variants (two of them byte-identical across the automations pages) collapse into formatClock, formatDurationWords and formatDurationMs on the date helper, all backed by dayjs.duration. Migrates the video badge, AI loading timer, media validation params, and the automation metrics/invocations tables onto them. Output is unchanged except the loading timer's minutes are no longer zero-padded (5:30, not 05:30).
This commit is contained in:
parent
1206448f17
commit
91b353c569
6 changed files with 55 additions and 41 deletions
|
|
@ -23,6 +23,7 @@ import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { formatBytes } from '@/composables/useMedia';
|
||||
import { getPlatformLabel, getPlatformLogo } from '@/composables/usePlatformLogo';
|
||||
import date from '@/date';
|
||||
import { classify, isDocument, isVideo, MediaType } from '@/lib/mediaType';
|
||||
import type { MediaItem } from '@/types/media';
|
||||
|
||||
|
|
@ -86,13 +87,6 @@ const openPreview = (item: MediaItem) => {
|
|||
);
|
||||
};
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const total = Math.round(seconds);
|
||||
const m = Math.floor(total / 60);
|
||||
const s = total % 60;
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const limitsWithUsage = computed(() =>
|
||||
props.platformLimits.map((p) => {
|
||||
const used = content.value.length;
|
||||
|
|
@ -267,7 +261,7 @@ const canRegenerateWithAi = (item: MediaItem): boolean => props.allowAiRegenerat
|
|||
class="inline-flex items-center gap-0.5 rounded-md bg-black/65 px-1.5 py-0.5 backdrop-blur-sm"
|
||||
>
|
||||
<IconVideo class="size-2.5" />
|
||||
{{ formatDuration(item.meta.duration) }}
|
||||
{{ date.formatClock(item.meta.duration) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.size"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getMediaRulesForContentType } from '@/composables/useMediaRules';
|
||||
import date from '@/date';
|
||||
import { isDocument, isGif, isImage, isVideo } from '@/lib/mediaType';
|
||||
import type { MediaItem } from '@/types/media';
|
||||
|
||||
|
|
@ -22,14 +23,6 @@ export const formatBytes = (bytes: number): string => {
|
|||
return bytes + ' B';
|
||||
};
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const s = Math.round(seconds);
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
const rem = s % 60;
|
||||
return rem === 0 ? `${m}min` : `${m}min ${rem}s`;
|
||||
};
|
||||
|
||||
const formatAspect = (ratio: number): string => ratio.toFixed(2);
|
||||
|
||||
/**
|
||||
|
|
@ -104,7 +97,7 @@ export const getMediaValidationWarning = (
|
|||
if (rules.maxVideoDurationSec && duration > rules.maxVideoDurationSec) {
|
||||
return {
|
||||
key: 'video_too_long',
|
||||
params: { max: formatDuration(rules.maxVideoDurationSec), current: formatDuration(duration) },
|
||||
params: { max: date.formatDurationWords(rules.maxVideoDurationSec), current: date.formatDurationWords(duration) },
|
||||
};
|
||||
}
|
||||
} else if (rules.maxImageBytes && size > rules.maxImageBytes) {
|
||||
|
|
|
|||
|
|
@ -214,4 +214,50 @@ export default {
|
|||
}
|
||||
return `${m}min`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format seconds as a clock (m:ss, or h:mm:ss past one hour).
|
||||
* For media badges and stopwatches (e.g. "5:30", "1:05:30").
|
||||
*/
|
||||
formatClock(seconds: number): string {
|
||||
const d = dayjs.duration(Math.round(seconds), 'seconds');
|
||||
|
||||
return d.asHours() >= 1 ? d.format('H:mm:ss') : d.format('m:ss');
|
||||
},
|
||||
|
||||
/**
|
||||
* Format seconds as short words (e.g. "45s", "5min", "5min 30s").
|
||||
*/
|
||||
formatDurationWords(seconds: number): string {
|
||||
const s = Math.round(seconds);
|
||||
if (s < 60) {
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
const m = Math.floor(s / 60);
|
||||
const rem = s % 60;
|
||||
|
||||
return rem === 0 ? `${m}min` : `${m}min ${rem}s`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format a duration in milliseconds (e.g. "—", "500ms", "1.5s", "2m 30s").
|
||||
*/
|
||||
formatDurationMs(ms: number | null): string {
|
||||
if (ms === null) {
|
||||
return '—';
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
|
||||
const seconds = ms / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -100,14 +100,6 @@ const summary = (invocation: Invocation): string => {
|
|||
return trans('automations.invocations.summary.pending');
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number | null): string => {
|
||||
if (ms === null) return '—';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const seconds = ms / 1000;
|
||||
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
||||
};
|
||||
|
||||
const stepsLabel = (count: number): string =>
|
||||
transChoice('automations.invocations.steps', count, {
|
||||
|
|
@ -362,7 +354,7 @@ const toggleExpand = async (invocation: Invocation) => {
|
|||
<TableCell
|
||||
class="text-right text-sm text-foreground/70 tabular-nums"
|
||||
>{{
|
||||
formatDuration(
|
||||
date.formatDurationMs(
|
||||
invocation.duration_ms,
|
||||
)
|
||||
}}</TableCell
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { computed, ref, watch } from 'vue';
|
|||
import AutomationDetailLayout from '@/components/automations/AutomationDetailLayout.vue';
|
||||
import AutomationRunsChart from '@/components/automations/AutomationRunsChart.vue';
|
||||
import { DateRangePicker } from '@/components/ui/date-range-picker';
|
||||
import date from '@/date';
|
||||
import dayjs from '@/dayjs';
|
||||
import type { Automation } from '@/types/automation/automation';
|
||||
|
||||
|
|
@ -47,22 +48,13 @@ watch(
|
|||
{ deep: true },
|
||||
);
|
||||
|
||||
const formatDuration = (ms: number | null): string => {
|
||||
if (ms === null) return '—';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const seconds = ms / 1000;
|
||||
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
||||
};
|
||||
|
||||
const cards = computed(() => [
|
||||
{ key: 'runs', label: 'automations.metrics.cards.runs', value: String(props.metrics.totals.runs) },
|
||||
{ key: 'completed', label: 'automations.metrics.cards.completed', value: String(props.metrics.totals.completed) },
|
||||
{ key: 'failed', label: 'automations.metrics.cards.failed', value: String(props.metrics.totals.failed) },
|
||||
{ key: 'in_progress', label: 'automations.metrics.cards.in_progress', value: String(props.metrics.totals.in_progress) },
|
||||
{ key: 'success_rate', label: 'automations.metrics.cards.success_rate', value: props.metrics.totals.success_rate === null ? '—' : `${props.metrics.totals.success_rate}%` },
|
||||
{ key: 'avg_duration', label: 'automations.metrics.cards.avg_duration', value: formatDuration(props.metrics.totals.avg_duration_ms) },
|
||||
{ key: 'avg_duration', label: 'automations.metrics.cards.avg_duration', value: date.formatDurationMs(props.metrics.totals.avg_duration_ms) },
|
||||
{ key: 'posts_created', label: 'automations.metrics.cards.posts_created', value: String(props.metrics.totals.posts_created) },
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { trans } from 'laravel-vue-i18n';
|
|||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import date from '@/date';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { calendar as calendarRoute } from '@/routes/app';
|
||||
import { create as createPostRoute, edit as editPostRoute } from '@/routes/app/posts';
|
||||
|
|
@ -53,11 +54,7 @@ const currentTip = computed(() => trans(tipKeys[tipIndex.value % tipKeys.length]
|
|||
const elapsed = ref(0);
|
||||
let elapsedTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const elapsedLabel = computed(() => {
|
||||
const minutes = Math.floor(elapsed.value / 60);
|
||||
const seconds = elapsed.value % 60;
|
||||
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
});
|
||||
const elapsedLabel = computed(() => date.formatClock(elapsed.value));
|
||||
|
||||
const progress = computed(() => {
|
||||
const ratio = elapsed.value / estimatedSeconds.value;
|
||||
|
|
|
|||
Loading…
Reference in a new issue