feat(kling): 优化时间轴对比组件样式和交互
Some checks failed
Build and Deploy / deploy (push) Has been cancelled

- 重构 TimelinePanel.vue 组件,使用 Tailwind CSS 替代 Less,简化样式代码
- 改进视觉设计:更新颜色方案、间距和图标,提升用户体验
- 移除音频结束位置标记,优化刻度尺和轨道显示逻辑
- 统一时长差异提示的样式和状态显示

feat(infra): 扩展文件预签名接口支持 Content-Type 参数

- 在 FileApi、FileClient、FileService 接口中新增带 Content-Type 参数的 presignGetUrl 方法
- 实现 S3FileClient 对 Content-Type 参数的支持,确保浏览器正确渲染媒体文件
- 在 TikUserFileServiceImpl 中为音视频文件生成预签名 URL 时自动推断 Content-Type
- 支持公开访问和私有访问两种模式下的 Content-Type 参数传递
This commit is contained in:
2026-04-09 01:04:07 +08:00
parent c607316f53
commit 63d3e7eecb
8 changed files with 118 additions and 290 deletions

View File

@@ -1,94 +1,94 @@
<template>
<div class="timeline-panel">
<div class="timeline-header">
<span class="timeline-title">时间轴对比</span>
<span v-if="showDurations" class="duration-badge">
人脸 {{ formatDuration(faceDurationMs) }}
<div class="bg-[#fafbfc] border border-[#e5e7eb] rounded-md px-4 py-3 mt-2">
<div class="flex justify-between items-center mb-2.5">
<span class="text-[13px] font-semibold text-gray-800">时间轴对比</span>
<span v-if="showDurations" class="flex items-center gap-1.5 text-[13px] text-gray-600 tabular-nums">
<span class="flex items-center gap-[5px]">
<span class="w-[7px] h-[7px] rounded-sm bg-blue-500"></span>
视频 {{ formatDuration(faceDurationMs) }}
</span>
<template v-if="audioDurationMs > 0">
<span class="divider"></span>
音频 {{ formatDuration(audioDurationMs) }}
<span class="text-gray-400 text-xs">/</span>
<span class="flex items-center gap-[5px]">
<span class="w-[7px] h-[7px] rounded-sm bg-emerald-500"></span>
音频 {{ formatDuration(audioDurationMs) }}
</span>
</template>
</span>
</div>
<!-- 刻度尺 -->
<div class="timeline-ruler">
<div class="relative h-5 mb-1.5 ml-12">
<div
v-for="mark in rulerMarks"
:key="mark.time"
class="ruler-mark"
class="absolute -translate-x-1/2 flex flex-col items-center"
:style="{ left: mark.position + '%' }"
>
<span class="ruler-label">{{ mark.label }}</span>
<span class="ruler-tick"></span>
</div>
<!-- 音频结束位置标记 -->
<div
v-if="audioDurationMs > 0 && isExceed"
class="audio-end-marker"
:style="{ left: audioEndPosition + '%' }"
>
<span class="audio-marker-label">{{ (audioDurationMs / 1000).toFixed(1) }}s</span>
<span class="audio-marker-line"></span>
<span class="text-[11px] text-gray-400 leading-none mb-1">{{ mark.label }}</span>
<span class="block w-px h-1 bg-[#e5e7eb]"></span>
</div>
</div>
<!-- 轨道区域 -->
<div class="timeline-tracks">
<div class="flex flex-col gap-1.5">
<!-- 视频轨道 -->
<div class="track">
<div class="track-info">
<span class="track-icon">📹</span>
<span class="track-label">视频</span>
<div class="flex items-center gap-2">
<div class="w-9 shrink-0">
<span class="text-xs text-gray-600 font-medium">视频</span>
</div>
<div class="track-bar">
<div class="flex-1 h-6 bg-[#f1f3f5] rounded relative overflow-hidden flex items-center">
<div
class="track-fill video-fill"
class="h-full rounded flex items-center justify-center transition-[width] duration-300 min-w-[2px] bg-blue-500"
:style="{ width: videoBarWidth + '%' }"
>
<span v-if="videoBarWidth > 15" class="track-time">{{ formatDuration(faceDurationMs) }}</span>
<span v-if="videoBarWidth > 20" class="text-xs text-white font-medium tracking-wide">{{ formatDuration(faceDurationMs) }}</span>
</div>
</div>
</div>
<!-- 音频轨道 -->
<div class="track">
<div class="track-info">
<span class="track-icon">🎙</span>
<span class="track-label">音频</span>
<div class="flex items-center gap-2">
<div class="w-9 shrink-0">
<span class="text-xs text-gray-600 font-medium">音频</span>
</div>
<div class="track-bar">
<div class="flex-1 h-6 bg-[#f1f3f5] rounded relative overflow-hidden flex items-center">
<div
v-if="audioDurationMs > 0"
class="track-fill audio-fill"
:class="{ 'audio-exceed': isExceed }"
class="h-full rounded flex items-center justify-center transition-[width] duration-300 min-w-[2px]"
:class="isExceed ? 'bg-red-500' : 'bg-emerald-500'"
:style="{ width: audioBarWidth + '%' }"
>
<span v-if="audioBarWidth > 15" class="track-time">{{ formatDuration(audioDurationMs) }}</span>
<span v-if="audioBarWidth > 20" class="text-xs text-white font-medium tracking-wide">{{ formatDuration(audioDurationMs) }}</span>
</div>
<span v-else class="track-placeholder">等待生成音频</span>
<span v-else class="text-xs text-gray-400 pl-2.5">等待生成音频</span>
</div>
</div>
</div>
<!-- 校验错误提示 -->
<div v-if="validationError" class="timeline-diff error">
<Icon icon="lucide:x-circle" class="diff-icon" />
<div v-if="validationError" class="flex items-center gap-1.5 mt-2.5 px-2.5 py-2 rounded text-xs font-medium text-red-500 bg-red-50 border border-red-200">
<Icon icon="lucide:x-circle" class="text-sm shrink-0" />
<span>{{ validationError }}</span>
</div>
<!-- 时长差异提示 -->
<div v-else-if="audioDurationMs > 0" class="timeline-diff" :class="diffStatus">
<div v-else-if="audioDurationMs > 0" class="flex items-center gap-1.5 mt-2.5 px-2.5 py-2 rounded text-xs font-medium border border-transparent"
:class="{
'bg-[#f0fdf4] border-[#bbf7d0] text-emerald-600': diffStatus === 'match',
'bg-red-50 border-red-200 text-red-500': diffStatus === 'exceed',
'bg-amber-50 border-amber-200 text-amber-500': diffStatus === 'short',
}"
>
<template v-if="diffStatus === 'match'">
<Icon icon="lucide:check-circle" class="diff-icon" />
<Icon icon="lucide:check-circle" class="text-sm shrink-0" />
<span>时长匹配良好可以生成</span>
</template>
<template v-else-if="diffStatus === 'exceed'">
<Icon icon="lucide:alert-circle" class="diff-icon" />
<Icon icon="lucide:alert-circle" class="text-sm shrink-0" />
<span>音频超出 {{ formatDuration(diffMs) }}建议缩短文案</span>
</template>
<template v-else-if="diffStatus === 'short'">
<Icon icon="lucide:info" class="diff-icon" />
<Icon icon="lucide:info" class="text-sm shrink-0" />
<span>音频较短可适当增加文案</span>
</template>
</div>
@@ -127,10 +127,6 @@ const audioBarWidth = computed(() =>
Math.min(100, (props.audioDurationMs / maxDuration.value) * 100)
)
const audioEndPosition = computed(() =>
Math.min(100, (props.audioDurationMs / maxDuration.value) * 100)
)
const isExceed = computed(() => props.audioDurationMs > props.faceDurationMs)
const diffMs = computed(() => Math.abs(props.audioDurationMs - props.faceDurationMs))
@@ -178,235 +174,3 @@ function calculateInterval(duration: number): number {
const formatDuration = formatDurationMs
</script>
<style scoped lang="less">
// 蓝紫主题配色 - 与主页面协调
@text-primary: #1e293b;
@text-secondary: #64748b;
@text-tertiary: #94a3b8;
@bg-subtle: rgba(59, 130, 246, 0.04);
@bg-hover: rgba(59, 130, 246, 0.08);
@border-light: rgba(59, 130, 246, 0.1);
@border-medium: rgba(59, 130, 246, 0.15);
@accent-blue: #3b82f6;
@accent-purple: #8b5cf6;
@accent-green: #10b981;
@accent-red: #ef4444;
@accent-orange: #f59e0b;
.timeline-panel {
background: @bg-subtle;
border-radius: 10px;
padding: 14px 18px;
margin-top: 8px;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.timeline-title {
font-size: 12px;
font-weight: 600;
color: @text-secondary;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.duration-badge {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: @text-tertiary;
.divider {
width: 4px;
height: 4px;
background: @border-medium;
border-radius: 50%;
}
}
// 刻度尺
.timeline-ruler {
position: relative;
height: 18px;
margin-bottom: 10px;
margin-left: 80px;
}
.ruler-mark {
position: absolute;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
}
.ruler-label {
font-size: 10px;
color: @text-tertiary;
margin-bottom: 2px;
}
.ruler-tick {
display: block;
width: 1px;
height: 4px;
background: @border-medium;
}
// 音频结束位置标记
.audio-end-marker {
position: absolute;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
z-index: 10;
top: 0;
}
.audio-marker-label {
font-size: 9px;
font-weight: 600;
color: @accent-red;
background: rgba(235, 87, 87, 0.1);
padding: 1px 5px;
border-radius: 3px;
white-space: nowrap;
margin-bottom: 2px;
}
.audio-marker-line {
width: 1px;
height: 6px;
background: @accent-red;
}
// 轨道区域
.timeline-tracks {
display: flex;
flex-direction: column;
gap: 8px;
}
.track {
display: flex;
align-items: center;
gap: 12px;
}
.track-info {
display: flex;
align-items: center;
gap: 6px;
width: 68px;
flex-shrink: 0;
}
.track-icon {
font-size: 14px;
line-height: 1;
}
.track-label {
font-size: 12px;
color: @text-secondary;
font-weight: 500;
}
.track-bar {
flex: 1;
height: 22px;
background: rgba(55, 53, 47, 0.06);
border-radius: 4px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
}
.track-fill {
height: 100%;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.track-time {
font-size: 10px;
color: rgba(255, 255, 255, 0.95);
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
letter-spacing: 0.3px;
}
.track-placeholder {
font-size: 11px;
color: @text-tertiary;
padding-left: 14px;
}
.video-fill {
background: linear-gradient(90deg, @accent-blue 0%, @accent-purple 100%);
}
.audio-fill {
background: linear-gradient(90deg, @accent-green 0%, #059669 100%);
&.audio-exceed {
background: linear-gradient(90deg, @accent-red 0%, #dc2626 100%);
animation: pulse-warning 2s ease-in-out infinite;
}
}
@keyframes pulse-warning {
0%, 100% { opacity: 1; }
50% { opacity: 0.75; }
}
// 差异提示
.timeline-diff {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 10px 14px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
.diff-icon {
font-size: 14px;
flex-shrink: 0;
}
&.match {
background: rgba(16, 185, 129, 0.08);
color: @accent-green;
}
&.exceed {
background: rgba(239, 68, 68, 0.08);
color: @accent-red;
}
&.short {
background: rgba(245, 158, 11, 0.08);
color: @accent-orange;
}
&.error {
background: rgba(239, 68, 68, 0.12);
color: @accent-red;
}
}
</style>