This commit is contained in:
海龙 2025-08-23 19:02:26 +08:00
parent b0e15c16c0
commit f41f3b35e4
5 changed files with 210 additions and 68 deletions

View File

@ -209,7 +209,11 @@ export interface CreateMovieProjectV3Request {
/**角色名 */
role_name: string;
/**角色描述 */
role_description: string;
role_description: {
name: string;
image_url: string;
character_analysis: Record<string, any>;
};
/**照片URL */
photo_url: string;
/**声音URL */

View File

@ -126,12 +126,11 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
}
try {
const character_briefs = {
const character_brief = {
name: selectedTemplate.storyRole[activeRoleIndex].role_name,
image_url: imageUrl,
character_analysis: JSON.parse(desc).character_analysis,
};
console.log("character_briefs", character_briefs);
const updatedTemplate = {
...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role, index) =>
@ -139,7 +138,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
? {
...role,
photo_url: imageUrl,
role_description: JSON.stringify(character_briefs),
role_description: character_brief,
}
: role
),

View File

@ -146,8 +146,12 @@ export interface StoryTemplateEntity {
storyRole: {
/**角色名 */
role_name: string;
/**角色描述 */
role_description: string;
/**角色描述 对应后端是单个的character_brief */
role_description: {
name: string;
image_url: string;
character_analysis: Record<string, any>;
};
/**照片URL */
photo_url: string;
/**声音URL */

View File

@ -72,6 +72,44 @@ const audioRecorderStyles = `
background: #2563eb;
transform: scale(1.1);
}
/* 音量滑块样式 */
.volume-slider {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
.volume-slider::-webkit-slider-track {
background: transparent;
height: 16px;
border-radius: 8px;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 0;
height: 0;
box-shadow: -100px 0 5px 100px #92ff77, -100px 0px 20px 100px #92ff77;
cursor: pointer;
}
.volume-slider::-moz-range-track {
background: transparent;
height: 16px;
border-radius: 8px;
border: none;
}
.volume-slider::-moz-range-thumb {
width: 0;
height: 0;
border-radius: 0;
border: none;
box-shadow: -100px 0 5px 100px #92ff77, -100px 0px 20px 100px #92ff77;
}
`;
interface AudioRecorderProps {
@ -229,8 +267,11 @@ export function AudioRecorder({
// 音量调节
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
setVolume(newVolume);
if (isMuted && newVolume > 0) {
// 确保音量值在 0-1 范围内
const clampedVolume = Math.max(0, Math.min(1, newVolume));
console.log("newVolume", newVolume, "clampedVolume", clampedVolume);
setVolume(clampedVolume);
if (isMuted && clampedVolume > 0) {
setIsMuted(false);
}
};
@ -446,7 +487,7 @@ export function AudioRecorder({
data-alt="audio-player-container"
>
{/* 大旋转的播放圆盘 */}
<div className=" absolute z-1 right-6 top-[50%] translate-y-[-50%] scale-[1.4]">
<div className=" absolute z-10 right-10 top-[45%] translate-y-[-50%] scale-[1.4]">
<div className="relative">
{/* 外层边框 - 唱片边缘 */}
<div
@ -460,7 +501,7 @@ export function AudioRecorder({
{/* 唱片主体 - 纯黑色唱片面 */}
<div
className={`absolute inset-2 w-24 h-24 rounded-full bg-black shadow-[inset_0_2px_8px_rgba(0,0,0,0.8)] ${
className={`absolute inset-2 w-24 h-24 rounded-full bg-[0,0,0,.4] shadow-[inset_0_2px_8px_rgba(0,0,0,0.8)] ${
isPlaying ? "animate-spin" : ""
}`}
style={{ animationDuration: "6s" }}
@ -526,9 +567,19 @@ export function AudioRecorder({
></div>
</div>
</div>
{/* 播放控制 */}
<button
onClick={togglePlay}
className="absolute z-10 right-[4.75rem] top-[45%] translate-y-[-50%] flex items-center justify-center w-10 h-10 bg-gradient-to-br from-[rgb(106,244,249)] to-[rgb(199,59,255)] text-white rounded-full shadow-[inset_0_1px_0_rgba(255,255,255,0.3),0_4px_12px_rgba(0,0,0,0.3)] hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.4),0_6px_20px_rgba(106,244,249,0.4)] transition-all duration-300 hover:scale-105 active:scale-95"
>
{isPlaying ? (
<Pause className="w-5 h-5 drop-shadow-sm" />
) : (
<Play className="w-5 h-5 drop-shadow-sm ml-0.5" />
)}
</button>
{/* 头部 - 只显示操作按钮 */}
<div className="relative z-10 flex justify-end gap-2 mb-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="relative -right-3 z-10 flex justify-end gap-2 mb-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button
onClick={handleDelete}
className="text-white/60 hover:text-red-400 transition-colors"
@ -547,62 +598,19 @@ export function AudioRecorder({
</button>
)}
</div>
{/* 播放控制 */}
<div className="relative z-10 flex items-center justify-center gap-4 mb-4 mt-12">
<button
onClick={togglePlay}
className="flex items-center justify-center w-12 h-12 bg-gradient-to-br from-[rgb(106,244,249)] to-[rgb(199,59,255)] text-white rounded-xl shadow-[inset_0_1px_0_rgba(255,255,255,0.3),0_4px_12px_rgba(0,0,0,0.3)] hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.4),0_6px_20px_rgba(106,244,249,0.4)] transition-all duration-300 hover:scale-105 active:scale-95"
>
{isPlaying ? (
<Pause className="w-5 h-5 drop-shadow-sm" />
) : (
<Play className="w-5 h-5 drop-shadow-sm ml-0.5" />
)}
</button>
{/* WaveSurfer 波形图区域 */}
<div className="h-16 flex-1 bg-white/[0.05] rounded-lg overflow-hidden">
<WaveformPlayer
audioUrl={audioUrl}
isPlaying={isPlaying}
onPlayStateChange={setIsPlaying}
volume={volume}
isMuted={isMuted}
/>
</div>
</div>
{/* 音频设置 */}
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-6">
{/* 音量控制 */}
<div className="flex items-center gap-3">
<div className="w-24 h-3 bg-white/[0.08] rounded-full overflow-hidden shadow-inner border border-white/[0.1] relative">
<div
className="h-full bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] rounded-full transition-all duration-300"
style={{ width: `${volume * 100}%` }}
/>
{/* 可拖拽的音量滑块 */}
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={handleVolumeChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
{/* 视觉滑块指示器 */}
<div
className="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white rounded-full shadow-lg border-2 border-[rgb(199,59,255)] pointer-events-none transition-all duration-200"
style={{ left: `calc(${volume * 100}% - 8px)` }}
/>
</div>
{/* 音频图标 */}
<div className="text-white/70 hover:text-white/90 transition-colors">
<Volume2 className="w-5 h-5" />
</div>
</div>
</div>
<div className="relative z-1 flex items-center justify-between px-2 mt-16 mb-2">
<VolumeSlider volume={volume} onVolumeChange={handleVolumeChange} />
</div>
{/* WaveSurfer 波形图区域 */}
<div className=" relative z-5 h-16 flex-1 bg-white/[0.05] rounded-lg overflow-hidden">
<WaveformPlayer
audioUrl={audioUrl}
isPlaying={isPlaying}
onPlayStateChange={setIsPlaying}
volume={volume}
isMuted={isMuted}
/>
</div>
</div>
</>
@ -721,3 +729,130 @@ function WaveformPlayer({
</div>
);
}
function VolumeSlider({
volume,
onVolumeChange,
}: {
volume: number;
onVolumeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
const [isHovered, setIsHovered] = useState(false);
return (
<div className="w-full h-full">
<style jsx>{`
/* From Uiverse.io by javierBarroso */
/* level settings 👇 */
.slider {
/* slider */
--slider-width: 60%;
--slider-height: 12px;
--slider-bg: rgba(82, 82, 82, 0.322);
--slider-border-radius: 4px;
/* level */
--level-color: rgb(106,244,249);
--level-transition-duration: 5s;
/* icon */
--icon-margin: 10px;
--icon-color: var(--slider-bg);
--icon-size: 20px;
position: relative;
left: -40px;
}
.slider {
position: relative;
cursor: pointer;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.slider .volume {
display: inline-block;
vertical-align: top;
margin-right: var(--icon-margin);
color: var(--icon-color);
width: var(--icon-size);
height: auto;
position: absolute;
left: 12px;
pointer-events: none;
transition-duration: 0.5s;
}
.slider .level {
position: relative;
left: -30px;
top: -40px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: var(--slider-width);
height: var(--slider-height);
background: var(--slider-bg);
overflow: hidden;
border-radius: var(--slider-border-radius);
-webkit-transition: height var(--level-transition-duration);
-o-transition: height var(--level-transition-duration);
transition: height var(--level-transition-duration);
cursor: inherit;
transform: rotate(270deg);
opacity: 0;
transition: opacity 0.3s ease;
}
.slider:hover .level {
opacity: 1;
}
.slider .level::-webkit-slider-thumb {
-webkit-appearance: none;
width: 0px;
height: 0px;
-webkit-box-shadow: -200px 0 0 200px var(--level-color);
box-shadow: -100px 0 5px 100px var(--level-color),
-100px 0px 20px 100px var(--level-color);
}
.slider .level:hover ~ .volume {
color: var(--level-color);
opacity: 0.6;
}
.slider .level::-moz-range-thumb {
width: 0;
height: 0;
border-radius: 0;
border: none;
box-shadow: -100px 0 5px 100px var(--level-color),
-100px 0px 20px 100px var(--level-color);
}
`}</style>
<label
className="slider"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<input
type="range"
className="level"
min="0"
max="1"
step="0.01"
value={volume}
onChange={onVolumeChange}
/>
<Volume2 className="text-[#777876]" />
</label>
</div>
);
}

View File

@ -365,7 +365,7 @@ const RenderTemplateStoryMode = ({
<GlobalLoad show={isLoading} progress={0}>
<div className="rounded-2xl min-h-min transition-all duration-700 ease-out">
{/* 弹窗头部 */}
<div className="flex gap-4 p-4 border-b border-white/[0.1]">
<div className="flex gap-4 px-4 pb-2 border-b border-white/[0.1]">
<h2 className="text-2xl font-bold text-white">
Template Story Selection
</h2>