仅看ai
This commit is contained in:
parent
006aa04a7f
commit
bb7fea71f1
@ -69,12 +69,7 @@
|
|||||||
<!-- 字幕区域(底部,不遮挡数字人主体) -->
|
<!-- 字幕区域(底部,不遮挡数字人主体) -->
|
||||||
<div class="subtitle-overlay">
|
<div class="subtitle-overlay">
|
||||||
<div class="subtitle-container">
|
<div class="subtitle-container">
|
||||||
<SubtitlePanel
|
<SubtitlePanel :ai-subtitle="currentAISubtitle" />
|
||||||
v-model:mode="subtitleMode"
|
|
||||||
:ai-subtitle="currentAISubtitle"
|
|
||||||
:user-subtitle="currentUserSubtitle"
|
|
||||||
:messages="chatMessages"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,57 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="subtitle-panel">
|
<div class="subtitle-panel">
|
||||||
<!-- 字幕模式下拉框 -->
|
|
||||||
<div class="subtitle-header">
|
|
||||||
<div class="dropdown-wrapper" :class="{ open: dropdownOpen }">
|
|
||||||
<button class="dropdown-trigger" @click="dropdownOpen = !dropdownOpen">
|
|
||||||
<span class="dropdown-label">{{ modeLabel }}</span>
|
|
||||||
<svg
|
|
||||||
class="dropdown-arrow"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<Transition name="dropdown">
|
|
||||||
<div v-if="dropdownOpen" class="dropdown-menu">
|
|
||||||
<button
|
|
||||||
v-for="opt in modeOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
class="dropdown-item"
|
|
||||||
:class="{ active: currentMode === opt.value }"
|
|
||||||
@click="selectMode(opt.value)"
|
|
||||||
>
|
|
||||||
<span class="item-icon">{{ opt.icon }}</span>
|
|
||||||
<span class="item-label">{{ opt.label }}</span>
|
|
||||||
<svg
|
|
||||||
v-if="currentMode === opt.value"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="3"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="20 6 9 17 4 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模式1:仅AI字幕 -->
|
|
||||||
<div
|
<div
|
||||||
v-if="currentMode === 'ai-only'"
|
|
||||||
class="subtitle-content ai-only-mode"
|
class="subtitle-content ai-only-mode"
|
||||||
ref="scrollContainerRef"
|
ref="scrollContainerRef"
|
||||||
>
|
>
|
||||||
@ -99,224 +48,23 @@
|
|||||||
等待AI回复...
|
等待AI回复...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模式2:对话气泡 -->
|
|
||||||
<div
|
|
||||||
v-if="currentMode === 'bubble'"
|
|
||||||
class="subtitle-content bubble-mode"
|
|
||||||
ref="scrollContainerRef"
|
|
||||||
>
|
|
||||||
<div v-if="messages.length === 0" class="subtitle-placeholder">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
等待对话开始...
|
|
||||||
</div>
|
|
||||||
<div class="chat-list" ref="chatListRef">
|
|
||||||
<div
|
|
||||||
v-for="msg in messages"
|
|
||||||
:key="msg.id"
|
|
||||||
:class="[
|
|
||||||
'chat-bubble',
|
|
||||||
msg.role === 'user' ? 'bubble-user' : 'bubble-ai',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="bubble-body">
|
|
||||||
<div
|
|
||||||
class="bubble-text"
|
|
||||||
:class="{
|
|
||||||
collapsed: isCollapsed(msg.id) && isLongText(msg.content),
|
|
||||||
'text-user': msg.role === 'user',
|
|
||||||
'text-ai': msg.role === 'ai',
|
|
||||||
}"
|
|
||||||
:ref="(el) => setMsgRef(msg.id, el)"
|
|
||||||
>
|
|
||||||
{{ msg.content }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="isLongText(msg.content)"
|
|
||||||
class="expand-btn"
|
|
||||||
@click="toggleCollapse(msg.id)"
|
|
||||||
>
|
|
||||||
{{ isCollapsed(msg.id) ? "展开全部" : "收起" }}
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
:style="{
|
|
||||||
transform: isCollapsed(msg.id)
|
|
||||||
? 'rotate(0deg)'
|
|
||||||
: 'rotate(180deg)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模式3:合并气泡 -->
|
|
||||||
<div
|
|
||||||
v-if="currentMode === 'merged'"
|
|
||||||
class="subtitle-content merged-mode"
|
|
||||||
ref="scrollContainerRef"
|
|
||||||
>
|
|
||||||
<div v-if="messages.length === 0" class="subtitle-placeholder">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
等待对话开始...
|
|
||||||
</div>
|
|
||||||
<div class="merged-list" ref="mergedListRef">
|
|
||||||
<div
|
|
||||||
v-for="(pair, idx) in pairedMessages"
|
|
||||||
:key="idx"
|
|
||||||
class="merged-bubble"
|
|
||||||
>
|
|
||||||
<div v-if="pair.user" class="merged-user-section">
|
|
||||||
<div
|
|
||||||
class="merged-text text-user"
|
|
||||||
:class="{
|
|
||||||
collapsed:
|
|
||||||
isCollapsed(pair.user.id) && isLongText(pair.user.content),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ pair.user.content }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="isLongText(pair.user.content)"
|
|
||||||
class="expand-btn"
|
|
||||||
@click="toggleCollapse(pair.user.id)"
|
|
||||||
>
|
|
||||||
{{ isCollapsed(pair.user.id) ? "展开全部" : "收起" }}
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
:style="{
|
|
||||||
transform: isCollapsed(pair.user.id)
|
|
||||||
? 'rotate(0deg)'
|
|
||||||
: 'rotate(180deg)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="pair.ai" class="merged-ai-section">
|
|
||||||
<div
|
|
||||||
class="merged-text text-ai"
|
|
||||||
:class="{
|
|
||||||
collapsed:
|
|
||||||
isCollapsed(pair.ai.id) && isLongText(pair.ai.content),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ pair.ai.content }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="isLongText(pair.ai.content)"
|
|
||||||
class="expand-btn"
|
|
||||||
@click="toggleCollapse(pair.ai.id)"
|
|
||||||
>
|
|
||||||
{{ isCollapsed(pair.ai.id) ? "展开全部" : "收起" }}
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
:style="{
|
|
||||||
transform: isCollapsed(pair.ai.id)
|
|
||||||
? 'rotate(0deg)'
|
|
||||||
: 'rotate(180deg)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { ref, watch, nextTick } from "vue";
|
||||||
ref,
|
|
||||||
computed,
|
|
||||||
watch,
|
|
||||||
nextTick,
|
|
||||||
onMounted,
|
|
||||||
onBeforeUnmount,
|
|
||||||
} from "vue";
|
|
||||||
import type { ChatMessage, SubtitleMode } from "@/types";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mode: SubtitleMode;
|
|
||||||
aiSubtitle: string;
|
aiSubtitle: string;
|
||||||
userSubtitle: string;
|
|
||||||
messages: ChatMessage[];
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
"update:mode": [value: SubtitleMode];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const currentMode = computed({
|
|
||||||
get: () => props.mode,
|
|
||||||
set: (val: SubtitleMode) => emit("update:mode", val),
|
|
||||||
});
|
|
||||||
|
|
||||||
const dropdownOpen = ref(false);
|
|
||||||
// 滚动容器ref:指向 .subtitle-content(实际可滚动容器)
|
|
||||||
const scrollContainerRef = ref<HTMLElement | null>(null);
|
const scrollContainerRef = ref<HTMLElement | null>(null);
|
||||||
const aiSubtitleBarRef = ref<HTMLElement | null>(null);
|
const aiSubtitleBarRef = ref<HTMLElement | null>(null);
|
||||||
const aiSubtitleTextRef = ref<HTMLElement | null>(null);
|
const aiSubtitleTextRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
// ===== AI字幕自适应字号 =====
|
|
||||||
const aiFontSize = ref(18);
|
const aiFontSize = ref(18);
|
||||||
const aiTextOverflow = ref(false);
|
const aiTextOverflow = ref(false);
|
||||||
|
|
||||||
// 计算AI字幕字号:根据文本长度动态调整
|
|
||||||
const computeAIFontSize = () => {
|
const computeAIFontSize = () => {
|
||||||
const text = props.aiSubtitle;
|
const text = props.aiSubtitle;
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@ -332,7 +80,6 @@ const computeAIFontSize = () => {
|
|||||||
else aiFontSize.value = 12;
|
else aiFontSize.value = 12;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检测AI字幕是否溢出
|
|
||||||
const checkAIOverflow = () => {
|
const checkAIOverflow = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const bar = aiSubtitleBarRef.value;
|
const bar = aiSubtitleBarRef.value;
|
||||||
@ -352,118 +99,20 @@ const scrollAIToBottom = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== 长文本折叠/展开 =====
|
const scrollToBottom = () => {
|
||||||
const COLLAPSE_THRESHOLD = 120; // 超过120字符视为长文本
|
const el = scrollContainerRef.value;
|
||||||
const collapsedIds = ref<Set<string>>(new Set());
|
if (!el) return;
|
||||||
const msgRefs = new Map<string, HTMLElement>();
|
el.scrollTo({
|
||||||
|
top: el.scrollHeight,
|
||||||
const isLongText = (text: string): boolean => text.length > COLLAPSE_THRESHOLD;
|
behavior: "smooth",
|
||||||
|
|
||||||
const isCollapsed = (id: string): boolean => collapsedIds.value.has(id);
|
|
||||||
|
|
||||||
const toggleCollapse = (id: string) => {
|
|
||||||
if (collapsedIds.value.has(id)) {
|
|
||||||
collapsedIds.value.delete(id);
|
|
||||||
} else {
|
|
||||||
collapsedIds.value.add(id);
|
|
||||||
}
|
|
||||||
// 触发响应式更新
|
|
||||||
collapsedIds.value = new Set(collapsedIds.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMsgRef = (id: string, el: any) => {
|
|
||||||
if (el) msgRefs.set(id, el as HTMLElement);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== 下拉框 =====
|
|
||||||
const modeOptions = [
|
|
||||||
{ value: "ai-only" as SubtitleMode, label: "AI字幕", icon: "💬" },
|
|
||||||
{ value: "bubble" as SubtitleMode, label: "对话气泡", icon: "💭" },
|
|
||||||
{ value: "merged" as SubtitleMode, label: "合并气泡", icon: "📋" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const modeLabel = computed(() => {
|
|
||||||
return (
|
|
||||||
modeOptions.find((o) => o.value === currentMode.value)?.label || "AI字幕"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectMode = (val: SubtitleMode) => {
|
|
||||||
currentMode.value = val;
|
|
||||||
dropdownOpen.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (!target.closest(".dropdown-wrapper")) {
|
|
||||||
dropdownOpen.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener("click", handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 消息配对 =====
|
|
||||||
const pairedMessages = computed(() => {
|
|
||||||
const pairs: { user: ChatMessage | null; ai: ChatMessage | null }[] = [];
|
|
||||||
let currentPair: { user: ChatMessage | null; ai: ChatMessage | null } = {
|
|
||||||
user: null,
|
|
||||||
ai: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const msg of props.messages) {
|
|
||||||
if (msg.role === "user") {
|
|
||||||
if (currentPair.user) {
|
|
||||||
pairs.push(currentPair);
|
|
||||||
currentPair = { user: msg, ai: null };
|
|
||||||
} else {
|
|
||||||
currentPair.user = msg;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentPair.ai = msg;
|
|
||||||
pairs.push(currentPair);
|
|
||||||
currentPair = { user: null, ai: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentPair.user || currentPair.ai) {
|
|
||||||
pairs.push(currentPair);
|
|
||||||
}
|
|
||||||
return pairs;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 监听变化 =====
|
|
||||||
|
|
||||||
// 模式切换时:立即刷新内容并滚动到底部
|
|
||||||
watch(
|
|
||||||
() => props.mode,
|
|
||||||
() => {
|
|
||||||
computeAIFontSize();
|
|
||||||
nextTick(() => {
|
|
||||||
checkAIOverflow();
|
|
||||||
scrollToBottom();
|
|
||||||
// AI模式切换后确保字幕滚动到底部
|
|
||||||
if (props.mode === "ai-only" && aiSubtitleBarRef.value) {
|
|
||||||
aiSubtitleBarRef.value.scrollTo({
|
|
||||||
top: aiSubtitleBarRef.value.scrollHeight,
|
|
||||||
behavior: "instant" as ScrollBehavior,
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// AI字幕变化时:自适应字号 + 检测溢出 + 自动滚动
|
|
||||||
watch(
|
watch(
|
||||||
() => props.aiSubtitle,
|
() => props.aiSubtitle,
|
||||||
() => {
|
() => {
|
||||||
computeAIFontSize();
|
computeAIFontSize();
|
||||||
checkAIOverflow();
|
checkAIOverflow();
|
||||||
// 自动滚动AI字幕到底部
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (aiSubtitleBarRef.value) {
|
if (aiSubtitleBarRef.value) {
|
||||||
aiSubtitleBarRef.value.scrollTo({
|
aiSubtitleBarRef.value.scrollTo({
|
||||||
@ -474,55 +123,6 @@ watch(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 消息变化时:自动滚动到底部 + 新消息默认折叠
|
|
||||||
watch(
|
|
||||||
() => props.messages.length,
|
|
||||||
(newLen, oldLen) => {
|
|
||||||
// 新增的消息默认折叠
|
|
||||||
if (newLen > (oldLen ?? 0)) {
|
|
||||||
for (let i = oldLen ?? 0; i < newLen; i++) {
|
|
||||||
const msg = props.messages[i];
|
|
||||||
if (msg && isLongText(msg.content)) {
|
|
||||||
collapsedIds.value.add(msg.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collapsedIds.value = new Set(collapsedIds.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 流式内容更新时:自动滚动到底部(消息数量不变但内容变化)
|
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
const msgs = props.messages;
|
|
||||||
if (msgs.length === 0) return "";
|
|
||||||
// 监听最后一条消息的内容变化,用于流式更新时自动滚动
|
|
||||||
return msgs[msgs.length - 1].content;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
nextTick(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将对话列表滚动到底部
|
|
||||||
* 兼容不同内容量:内容少时无需滚动,内容超出可见区域时平滑滚动到底部
|
|
||||||
*/
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
const el = scrollContainerRef.value;
|
|
||||||
if (!el) return;
|
|
||||||
el.scrollTo({
|
|
||||||
top: el.scrollHeight,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -532,109 +132,6 @@ const scrollToBottom = () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 下拉框 ===== */
|
|
||||||
.subtitle-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-trigger {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(255, 255, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper.open & {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(108, 140, 255, 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-arrow {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
.dropdown-wrapper.open & {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 8px);
|
|
||||||
left: 0;
|
|
||||||
min-width: 180px;
|
|
||||||
background: rgba(20, 25, 50, 0.97);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 6px;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.15s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background: rgba(108, 140, 255, 0.15);
|
|
||||||
color: #6c8cff;
|
|
||||||
}
|
|
||||||
.item-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.item-label {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-enter-active,
|
|
||||||
.dropdown-leave-active {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
.dropdown-enter-from,
|
|
||||||
.dropdown-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(6px) scale(0.96);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 字幕内容 ===== */
|
|
||||||
.subtitle-content {
|
.subtitle-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@ -672,7 +169,6 @@ const scrollToBottom = () => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 模式1:AI字幕 ===== */
|
|
||||||
.ai-only-mode {
|
.ai-only-mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
@ -737,171 +233,4 @@ const scrollToBottom = () => {
|
|||||||
transform: translateX(-50%) scale(1.1);
|
transform: translateX(-50%) scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 模式2:对话气泡 ===== */
|
|
||||||
.bubble-mode {
|
|
||||||
.chat-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-bubble {
|
|
||||||
animation: bubbleSlide 0.35s ease;
|
|
||||||
|
|
||||||
&.bubble-user {
|
|
||||||
.bubble-body {
|
|
||||||
background: rgba(240, 242, 255, 0.92);
|
|
||||||
border-color: rgba(79, 110, 247, 0.12);
|
|
||||||
border-radius: 14px 14px 4px 14px;
|
|
||||||
margin-left: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bubble-ai {
|
|
||||||
.bubble-body {
|
|
||||||
background: rgba(20, 30, 50, 0.85);
|
|
||||||
border-color: rgba(52, 211, 153, 0.12);
|
|
||||||
border-radius: 14px 14px 14px 4px;
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bubbleSlide {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px) scale(0.96);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-body {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.35s ease;
|
|
||||||
|
|
||||||
&.text-user {
|
|
||||||
color: #1a1a1a;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.text-ai {
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
max-height: 4.8em;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2em;
|
|
||||||
background: linear-gradient(transparent, rgba(10, 14, 30, 0.5));
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 展开/收起按钮 ===== */
|
|
||||||
.expand-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
margin-top: 6px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
font-size: 11px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
border-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 模式3:合并气泡 ===== */
|
|
||||||
.merged-mode {
|
|
||||||
.merged-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.merged-bubble {
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
animation: bubbleSlide 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.merged-user-section {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-bottom: 1px solid rgba(79, 110, 247, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.merged-ai-section {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.merged-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.35s ease;
|
|
||||||
|
|
||||||
&.text-user {
|
|
||||||
color: #303030ab;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.text-ai {
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
max-height: 4.8em;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2em;
|
|
||||||
background: linear-gradient(transparent, rgba(10, 14, 30, 0.5));
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -14,8 +14,8 @@ const SILENCE_TIMEOUT_MS = 5000;
|
|||||||
const MIN_VALID_DURATION_SEC = 0.5;
|
const MIN_VALID_DURATION_SEC = 0.5;
|
||||||
// 播报超时保护(毫秒):如果FINISHED回调未触发,自动恢复状态
|
// 播报超时保护(毫秒):如果FINISHED回调未触发,自动恢复状态
|
||||||
const BROADCAST_TIMEOUT_MS = 60000;
|
const BROADCAST_TIMEOUT_MS = 60000;
|
||||||
const APPID = "i-sfkmd7h8ex6nd";
|
const APPID = "i-sfqpfp8qayj8e";
|
||||||
const APP_KEY = "hs6b9nu9b080ap9hswgp";
|
const APP_KEY = "rdjqhnp5q50e010wvw0z";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测文本是否包含实质内容(去除空白和标点后仍有字符)
|
* 检测文本是否包含实质内容(去除空白和标点后仍有字符)
|
||||||
@ -496,8 +496,8 @@ export function useDigitalHuman(options?: {
|
|||||||
token: token.value,
|
token: token.value,
|
||||||
wrapperId: "human-wrapper",
|
wrapperId: "human-wrapper",
|
||||||
connectParams: {
|
connectParams: {
|
||||||
ttsPer: "CAP_4189",
|
ttsPer: "5135",
|
||||||
figureId: "3999826",
|
figureId: "4000288",
|
||||||
resolutionHeight: 1920,
|
resolutionHeight: 1920,
|
||||||
resolutionWidth: 1080,
|
resolutionWidth: 1080,
|
||||||
inactiveDisconnectSec: 300,
|
inactiveDisconnectSec: 300,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user