work-flow视频修改按钮通过video_modification来显示

This commit is contained in:
qikongjian 2025-09-24 15:56:55 +08:00
parent 7e49b48d0f
commit 3e176bc265
8 changed files with 569 additions and 153 deletions

View File

@ -1,5 +1,5 @@
NEXT_PUBLIC_JAVA_URL = https://auth.test.movieflow.ai
NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com # NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com

View File

@ -60,6 +60,23 @@ export async function POST(request: NextRequest) {
}; };
break; break;
case 'video_modification':
// 视频修改功能配置 - 控制视频编辑笔图标显示
// 可以通过查询参数 ?show=false 来测试隐藏功能
const url = new URL(request.url);
const showParam = url.searchParams.get('show');
const showValue = showParam !== null ? showParam === 'true' : true; // 默认显示
responseData = {
id: 9,
code: 'video_modification',
value: `{\n "show": ${showValue}\n}`,
note: '视频修改功能开关',
updated_at: new Date().toISOString().slice(0, 19)
};
console.log('📋 video_modification配置:', { showParam, showValue, value: responseData.value });
break;
default: default:
// 默认返回空配置 // 默认返回空配置
responseData = { responseData = {

View File

@ -0,0 +1,112 @@
'use client';
import React, { useState, useEffect } from 'react';
import { isVideoModificationEnabled, isGoogleLoginEnabled } from '@/lib/server-config';
export default function TestServerConfigPage() {
const [ssoStatus, setSsoStatus] = useState<boolean | null>(null);
const [videoModStatus, setVideoModStatus] = useState<boolean | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const testConfigs = async () => {
setLoading(true);
setError(null);
try {
console.log('🧪 开始测试服务器配置...');
// 测试SSO配置
const ssoEnabled = await isGoogleLoginEnabled();
console.log('📋 SSO配置结果:', ssoEnabled);
setSsoStatus(ssoEnabled);
// 测试视频修改配置
const videoModEnabled = await isVideoModificationEnabled();
console.log('📋 视频修改配置结果:', videoModEnabled);
setVideoModStatus(videoModEnabled);
console.log('✅ 所有配置测试完成');
} catch (err) {
console.error('❌ 配置测试失败:', err);
setError(err instanceof Error ? err.message : '未知错误');
} finally {
setLoading(false);
}
};
useEffect(() => {
testConfigs();
}, []);
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-center"></h1>
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-gray-50 rounded">
<span className="font-medium">Google登录 (sso_config):</span>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
ssoStatus === null ? 'bg-gray-200 text-gray-600' :
ssoStatus ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800'
}`}>
{ssoStatus === null ? '检测中...' : ssoStatus ? '启用' : '禁用'}
</span>
</div>
<div className="flex items-center justify-between p-4 bg-gray-50 rounded">
<span className="font-medium"> (video_modification):</span>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
videoModStatus === null ? 'bg-gray-200 text-gray-600' :
videoModStatus ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800'
}`}>
{videoModStatus === null ? '检测中...' : videoModStatus ? '启用' : '禁用'}
</span>
</div>
</div>
{error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded">
<p className="text-red-800 font-medium">:</p>
<p className="text-red-600">{error}</p>
</div>
)}
<div className="mt-6 flex gap-4">
<button
onClick={testConfigs}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? '测试中...' : '重新测试'}
</button>
<button
onClick={() => window.location.href = '/movies/work-flow'}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
Work-Flow页面
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">API测试信息</h2>
<div className="space-y-2 text-sm text-gray-600">
<p><strong>SSO API:</strong> POST /api/server-setting/find_by_code {"{ code: 'sso_config' }"}</p>
<p><strong>API:</strong> POST /api/server-setting/find_by_code {"{ code: 'video_modification' }"}</p>
<p><strong>:</strong> {"{ code: 0, successful: true, data: { value: '{\"show\": true}' } }"}</p>
</div>
</div>
<div className="mt-6 text-center text-sm text-gray-500">
<p>API调用日志</p>
</div>
</div>
</div>
);
}

View File

@ -14,6 +14,7 @@ import { Button, Tooltip } from 'antd';
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
import { VideoEditOverlay } from './video-edit/VideoEditOverlay'; import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
import { EditPoint as EditPointType } from './video-edit/types'; import { EditPoint as EditPointType } from './video-edit/types';
import { isVideoModificationEnabled } from '@/lib/server-config';
interface MediaViewerProps { interface MediaViewerProps {
taskObject: TaskObject; taskObject: TaskObject;
@ -78,6 +79,8 @@ export const MediaViewer = React.memo(function MediaViewer({
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false); const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false);
const [isVideoEditMode, setIsVideoEditMode] = useState(false); const [isVideoEditMode, setIsVideoEditMode] = useState(false);
// 控制钢笔图标显示的状态 - 参考谷歌登录按钮的实现
const [showVideoModification, setShowVideoModification] = useState(false);
useEffect(() => { useEffect(() => {
if (isSmartChatBoxOpen) { if (isSmartChatBoxOpen) {
@ -89,6 +92,33 @@ export const MediaViewer = React.memo(function MediaViewer({
} }
}, [isSmartChatBoxOpen]) }, [isSmartChatBoxOpen])
// 检查视频修改功能是否启用 - 参考谷歌登录按钮的实现
useEffect(() => {
const checkVideoModificationStatus = async () => {
try {
console.log('🔍 MediaViewer开始检查视频修改功能状态...');
const enabled = await isVideoModificationEnabled();
console.log('📋 MediaViewer视频修改功能启用状态:', enabled);
setShowVideoModification(enabled);
console.log('📋 MediaViewer设置showVideoModification状态为:', enabled);
} catch (error) {
console.error("❌ MediaViewerFailed to check video modification status:", error);
setShowVideoModification(false); // 出错时默认不显示
}
};
checkVideoModificationStatus();
}, []); // 只在组件挂载时执行一次
// 调试:监控钢笔图标显示状态
useEffect(() => {
console.log('🔧 MediaViewer状态更新:', {
enableVideoEdit,
showVideoModification,
shouldShowPenIcon: enableVideoEdit && showVideoModification
});
}, [enableVideoEdit, showVideoModification]);
// 音量控制函数 // 音量控制函数
const toggleMute = () => { const toggleMute = () => {
setUserHasInteracted(true); setUserHasInteracted(true);
@ -526,17 +556,20 @@ export const MediaViewer = React.memo(function MediaViewer({
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{ <div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: toosBtnRight right: toosBtnRight
}}> }}>
{/* 视频编辑模式切换按钮 - 临时注释 */} {/* 视频编辑模式切换按钮 - 通过服务器配置控制显示 */}
{/* {enableVideoEdit && ( {enableVideoEdit && showVideoModification && (
<Tooltip placement="top" title={isVideoEditMode ? "Exit edit mode" : "Enter edit mode"}> <Tooltip placement="top" title={isVideoEditMode ? "Exit edit mode" : "Enter edit mode"}>
<GlassIconButton <GlassIconButton
icon={PenTool} icon={PenTool}
size='sm' size='sm'
onClick={() => setIsVideoEditMode(!isVideoEditMode)} onClick={() => {
console.log('🖊️ 钢笔图标被点击,切换编辑模式:', !isVideoEditMode);
setIsVideoEditMode(!isVideoEditMode);
}}
className={isVideoEditMode ? 'bg-blue-500/20 border-blue-500/50' : ''} className={isVideoEditMode ? 'bg-blue-500/20 border-blue-500/50' : ''}
/> />
</Tooltip> </Tooltip>
)} */} )}
{/* 添加到chat去编辑 按钮 */} {/* 添加到chat去编辑 按钮 */}
<Tooltip placement="top" title="Edit video with chat"> <Tooltip placement="top" title="Edit video with chat">
<GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => { <GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => {

View File

@ -6,6 +6,13 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ConnectionPathParams, InputBoxPosition } from './types'; import { ConnectionPathParams, InputBoxPosition } from './types';
import {
CONNECTION_STYLE,
ARROW_GEOMETRY,
calculateArrowGeometry,
calculateCurvePath as calculateUnifiedCurvePath,
getConnectionAnimationConfig
} from './connection-config';
interface EditConnectionProps { interface EditConnectionProps {
/** 起始点坐标(编辑点位置) */ /** 起始点坐标(编辑点位置) */
@ -95,7 +102,8 @@ export function calculateInputPosition(
direction = 'right'; direction = 'right';
inputX = pointX + connectionLength; inputX = pointX + connectionLength;
inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2)); inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2));
connectionEndX = inputX; // 箭头指向输入框左边缘的中心
connectionEndX = inputX - 8; // 向内偏移8px指向输入框内部
connectionEndY = inputY + inputHeight / 2; connectionEndY = inputY + inputHeight / 2;
} }
// 其次选择左侧 // 其次选择左侧
@ -103,7 +111,8 @@ export function calculateInputPosition(
direction = 'left'; direction = 'left';
inputX = pointX - connectionLength - inputWidth; inputX = pointX - connectionLength - inputWidth;
inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2)); inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2));
connectionEndX = inputX + inputWidth; // 箭头指向输入框右边缘的中心
connectionEndX = inputX + inputWidth + 8; // 向内偏移8px指向输入框内部
connectionEndY = inputY + inputHeight / 2; connectionEndY = inputY + inputHeight / 2;
} }
// 然后选择下方 // 然后选择下方
@ -111,23 +120,26 @@ export function calculateInputPosition(
direction = 'bottom'; direction = 'bottom';
inputX = Math.max(margin, Math.min(containerWidth - inputWidth - margin, pointX - inputWidth / 2)); inputX = Math.max(margin, Math.min(containerWidth - inputWidth - margin, pointX - inputWidth / 2));
inputY = pointY + connectionLength; inputY = pointY + connectionLength;
// 箭头指向输入框上边缘的中心
connectionEndX = inputX + inputWidth / 2; connectionEndX = inputX + inputWidth / 2;
connectionEndY = inputY; connectionEndY = inputY - 8; // 向内偏移8px指向输入框内部
} }
// 最后选择上方 // 最后选择上方
else if (spaceTop >= inputHeight + connectionLength + margin) { else if (spaceTop >= inputHeight + connectionLength + margin) {
direction = 'top'; direction = 'top';
inputX = Math.max(margin, Math.min(containerWidth - inputWidth - margin, pointX - inputWidth / 2)); inputX = Math.max(margin, Math.min(containerWidth - inputWidth - margin, pointX - inputWidth / 2));
inputY = pointY - connectionLength - inputHeight; inputY = pointY - connectionLength - inputHeight;
// 箭头指向输入框下边缘的中心
connectionEndX = inputX + inputWidth / 2; connectionEndX = inputX + inputWidth / 2;
connectionEndY = inputY + inputHeight; connectionEndY = inputY + inputHeight + 8; // 向内偏移8px指向输入框内部
} }
// 如果空间不足,强制放在右侧并调整位置 // 如果空间不足,强制放在右侧并调整位置
else { else {
direction = 'right'; direction = 'right';
inputX = Math.min(containerWidth - inputWidth - margin, pointX + 40); inputX = Math.min(containerWidth - inputWidth - margin, pointX + 40);
inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2)); inputY = Math.max(margin, Math.min(containerHeight - inputHeight - margin, pointY - inputHeight / 2));
connectionEndX = inputX; // 箭头指向输入框左边缘的中心
connectionEndX = inputX - 8; // 向内偏移8px指向输入框内部
connectionEndY = inputY + inputHeight / 2; connectionEndY = inputY + inputHeight / 2;
} }
@ -150,80 +162,30 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
curvature = 0.3, curvature = 0.3,
animated = true animated = true
}) => { }) => {
// 使用统一的样式配置
const { const {
color = 'rgba(255, 255, 255, 0.9)', // White color to match the reference image color = CONNECTION_STYLE.color,
strokeWidth = 2, strokeWidth = CONNECTION_STYLE.strokeWidth,
dashArray = '8,4' // Dashed line to match the reference image dashArray = CONNECTION_STYLE.dashArray
} = style; } = style;
// 计算箭头几何参数 // 使用统一的箭头几何计算
const arrowSize = 8; const arrowGeometry = useMemo(() =>
const arrowHalfHeight = 4; calculateArrowGeometry(startPoint, endPoint),
[startPoint, endPoint]
);
// 计算连接方向和角度 // 使用统一的路径计算
const connectionVector = useMemo(() => {
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const length = Math.sqrt(dx * dx + dy * dy);
return {
dx: dx / length,
dy: dy / length,
angle: Math.atan2(dy, dx)
};
}, [startPoint, endPoint]);
// 计算箭头的正确位置和线条终点
const arrowGeometry = useMemo(() => {
const { dx, dy, angle } = connectionVector;
// 箭头尖端位置原endPoint
const arrowTip = { x: endPoint.x, y: endPoint.y };
// 箭头底部中心点(线条应该连接到这里)
const arrowBase = {
x: endPoint.x - dx * arrowSize,
y: endPoint.y - dy * arrowSize
};
// 计算箭头三角形的三个顶点
const perpX = -dy; // 垂直向量X
const perpY = dx; // 垂直向量Y
const arrowPoints = [
arrowTip, // 尖端
{
x: arrowBase.x + perpX * arrowHalfHeight,
y: arrowBase.y + perpY * arrowHalfHeight
},
{
x: arrowBase.x - perpX * arrowHalfHeight,
y: arrowBase.y - perpY * arrowHalfHeight
}
];
return {
tip: arrowTip,
base: arrowBase,
points: arrowPoints,
angle
};
}, [endPoint, connectionVector, arrowSize, arrowHalfHeight]);
// 计算路径(线条终止于箭头底部中心)
const path = useMemo(() => const path = useMemo(() =>
calculateCurvePath({ calculateUnifiedCurvePath(startPoint, arrowGeometry.center, containerSize),
start: startPoint, [startPoint, arrowGeometry.center, containerSize]
end: arrowGeometry.base, // 连接到箭头底部中心而不是尖端 );
containerSize,
curvature
}), [startPoint, arrowGeometry.base, containerSize, curvature]);
// 计算路径长度用于动画 // 获取统一的动画配置
const pathLength = useMemo(() => { const animationConfig = useMemo(() =>
const dx = arrowGeometry.base.x - startPoint.x; getConnectionAnimationConfig(animated),
const dy = arrowGeometry.base.y - startPoint.y; [animated]
return Math.sqrt(dx * dx + dy * dy) * 1.2; // 弧线比直线长约20% );
}, [startPoint, arrowGeometry.base]);
return ( return (
<svg <svg
@ -232,7 +194,7 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
height={containerSize.height} height={containerSize.height}
style={{ zIndex: 10 }} style={{ zIndex: 10 }}
> >
{/* Curved dashed line - properly aligned to arrow base center */} {/* 统一的虚线连接线 - 精确连接到箭头中心 */}
<motion.path <motion.path
d={path} d={path}
fill="none" fill="none"
@ -241,44 +203,23 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
strokeDasharray={dashArray} strokeDasharray={dashArray}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
initial={animated ? { initial={animationConfig.line.initial}
pathLength: 0, animate={animationConfig.line.animate}
opacity: 0 transition={animationConfig.line.transition}
} : {}}
animate={animated ? {
pathLength: 1,
opacity: 1
} : {}}
transition={animated ? {
pathLength: { duration: 0.6, ease: "easeInOut" },
opacity: { duration: 0.3 }
} : {}}
style={{ style={{
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))' filter: CONNECTION_STYLE.dropShadow
}} }}
/> />
{/* Properly aligned arrow head with geometric precision */} {/* 几何精确的箭头 - 与连接线完美对齐 */}
<motion.polygon <motion.polygon
points={arrowGeometry.points.map(p => `${p.x},${p.y}`).join(' ')} points={arrowGeometry.points.map(p => `${p.x},${p.y}`).join(' ')}
fill={color} fill={color}
initial={animated ? { initial={animationConfig.arrow.initial}
scale: 0, animate={animationConfig.arrow.animate}
opacity: 0 transition={animationConfig.arrow.transition}
} : {}}
animate={animated ? {
scale: 1,
opacity: 1
} : {}}
transition={animated ? {
delay: 0.4,
duration: 0.3,
type: "spring",
stiffness: 300,
damping: 25
} : {}}
style={{ style={{
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))' filter: CONNECTION_STYLE.dropShadow
}} }}
/> />

View File

@ -6,6 +6,12 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { EditPoint as EditPointType, EditPointStatus } from './types'; import { EditPoint as EditPointType, EditPointStatus } from './types';
import {
CONNECTION_STYLE,
calculateArrowGeometry,
calculateCurvePath,
getConnectionAnimationConfig
} from './connection-config';
interface EditDescriptionProps { interface EditDescriptionProps {
/** 编辑点数据 */ /** 编辑点数据 */
@ -42,25 +48,31 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
y: (editPoint.position.y / 100) * containerSize.height y: (editPoint.position.y / 100) * containerSize.height
}), [editPoint.position, containerSize]); }), [editPoint.position, containerSize]);
// 计算连接线路径 // 使用统一的连接线几何计算
const connectionPath = useMemo(() => { const connectionGeometry = useMemo(() => {
const startX = editPointPosition.x; const startPoint = { x: editPointPosition.x, y: editPointPosition.y };
const startY = editPointPosition.y; const endPoint = { x: connectionEnd.x, y: connectionEnd.y };
const endX = connectionEnd.x;
const endY = connectionEnd.y;
// 计算控制点,创建优雅的弧线 // 使用统一的箭头几何计算
const deltaX = endX - startX; const arrowGeometry = calculateArrowGeometry(startPoint, endPoint);
const deltaY = endY - startY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 控制点偏移量,创建自然的弧线 // 使用统一的路径计算
const controlOffset = Math.min(distance * 0.3, 60); const path = calculateCurvePath(startPoint, arrowGeometry.center, containerSize);
const controlX = startX + deltaX * 0.5 + (deltaY > 0 ? -controlOffset : controlOffset);
const controlY = startY + deltaY * 0.5 - Math.abs(deltaX) * 0.2;
return `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`; return {
}, [editPointPosition, connectionEnd]); path,
arrowPoints: arrowGeometry.points,
arrowTip: arrowGeometry.tip,
arrowBase: arrowGeometry.base,
arrowCenter: arrowGeometry.center
};
}, [editPointPosition, connectionEnd, containerSize]);
// 获取统一的动画配置
const animationConfig = useMemo(() =>
getConnectionAnimationConfig(true), // EditDescription总是使用动画
[]
);
// 获取状态颜色 // 获取状态颜色
const getStatusColor = () => { const getStatusColor = () => {
@ -101,7 +113,7 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
<AnimatePresence> <AnimatePresence>
{editPoint.description && editPoint.status !== EditPointStatus.PENDING && ( {editPoint.description && editPoint.status !== EditPointStatus.PENDING && (
<> <>
{/* White dashed connection line to match reference image */} {/* 统一的虚线连接线 - 与EditConnection完全一致 */}
<motion.svg <motion.svg
className="absolute pointer-events-none" className="absolute pointer-events-none"
style={{ style={{
@ -116,38 +128,39 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
{/* 统一的虚线连接线 - 与EditConnection完全一致 */}
<motion.path <motion.path
d={connectionPath} d={connectionGeometry.path}
stroke="rgba(255, 255, 255, 0.9)" stroke={CONNECTION_STYLE.color}
strokeWidth={2} strokeWidth={CONNECTION_STYLE.strokeWidth}
fill="none" fill="none"
strokeDasharray="8,4" strokeDasharray={CONNECTION_STYLE.dashArray}
strokeLinecap="round" strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0 }} strokeLinejoin="round"
animate={{ initial={animationConfig.line?.initial}
pathLength: 1, animate={animationConfig.line?.animate}
opacity: 1 exit={animationConfig.line?.initial}
}}
exit={{ pathLength: 0, opacity: 0 }}
transition={{ transition={{
...animationConfig.line?.transition,
// 稍微延长显示状态的动画时间
pathLength: { duration: 0.8, ease: "easeOut" }, pathLength: { duration: 0.8, ease: "easeOut" },
opacity: { duration: 0.5 } opacity: { duration: 0.5 }
}} }}
style={{ style={{
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))' filter: CONNECTION_STYLE.dropShadow
}} }}
/> />
{/* Arrow head */} {/* 几何精确的箭头 - 与连接线完美对齐 */}
<motion.polygon <motion.polygon
points={`${connectionEnd.x},${connectionEnd.y} ${connectionEnd.x-8},${connectionEnd.y-4} ${connectionEnd.x-8},${connectionEnd.y+4}`} points={connectionGeometry.arrowPoints.map(p => `${p.x},${p.y}`).join(' ')}
fill="rgba(255, 255, 255, 0.9)" fill={CONNECTION_STYLE.color}
initial={{ scale: 0, opacity: 0 }} initial={animationConfig.arrow?.initial}
animate={{ scale: 1, opacity: 1 }} animate={animationConfig.arrow?.animate}
exit={{ scale: 0, opacity: 0 }} exit={animationConfig.arrow?.initial}
transition={{ delay: 0.4, duration: 0.3 }} transition={animationConfig.arrow?.transition}
style={{ style={{
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))' filter: CONNECTION_STYLE.dropShadow
}} }}
/> />
</motion.svg> </motion.svg>

View File

@ -0,0 +1,196 @@
/**
* 线
* 线使
*/
/**
* 线
*/
export const CONNECTION_STYLE = {
// 颜色配置
color: 'rgba(255, 255, 255, 0.9)', // 统一的白色,确保在深色背景下清晰可见
strokeWidth: 2, // 统一的线条粗细
dashArray: '8,4', // 统一的虚线样式8px实线4px间隔
// 阴影效果
dropShadow: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))',
// 动画配置
animation: {
pathDuration: 0.6,
pathEasing: 'easeInOut',
opacityDuration: 0.3,
arrowDelay: 0.4,
arrowDuration: 0.3,
springConfig: {
stiffness: 300,
damping: 25
}
}
} as const;
/**
*
*/
export const ARROW_GEOMETRY = {
size: 8, // 箭头长度
halfHeight: 4, // 箭头半高(宽度的一半)
centerOffset: 0.6 // 连接线连接到箭头的位置比例0.6表示稍微向前偏移)
} as const;
/**
* 线
*/
export const CURVE_CONFIG = {
curvature: 0.3, // 弧线弯曲程度
minControlOffset: 10, // 最小控制点偏移
maxControlOffset: 60 // 最大控制点偏移
} as const;
/**
*
*/
export function calculateArrowGeometry(
startPoint: { x: number; y: number },
endPoint: { x: number; y: number }
) {
// 计算连接方向向量
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const normalizedDx = dx / distance;
const normalizedDy = dy / distance;
// 箭头几何计算
const arrowTip = { x: endPoint.x, y: endPoint.y };
const arrowBase = {
x: endPoint.x - normalizedDx * ARROW_GEOMETRY.size,
y: endPoint.y - normalizedDy * ARROW_GEOMETRY.size
};
const arrowCenter = {
x: endPoint.x - normalizedDx * (ARROW_GEOMETRY.size * ARROW_GEOMETRY.centerOffset),
y: endPoint.y - normalizedDy * (ARROW_GEOMETRY.size * ARROW_GEOMETRY.centerOffset)
};
// 计算垂直向量用于箭头宽度
const perpX = -normalizedDy;
const perpY = normalizedDx;
const arrowPoints = [
arrowTip,
{
x: arrowBase.x + perpX * ARROW_GEOMETRY.halfHeight,
y: arrowBase.y + perpY * ARROW_GEOMETRY.halfHeight
},
{
x: arrowBase.x - perpX * ARROW_GEOMETRY.halfHeight,
y: arrowBase.y - perpY * ARROW_GEOMETRY.halfHeight
}
];
return {
tip: arrowTip,
base: arrowBase,
center: arrowCenter,
points: arrowPoints,
direction: { dx: normalizedDx, dy: normalizedDy },
perpendicular: { perpX, perpY },
distance
};
}
/**
* 线
*/
export function calculateCurvePath(
startPoint: { x: number; y: number },
endPoint: { x: number; y: number },
containerSize: { width: number; height: number }
): string {
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 计算控制点,创建优雅的弧线
const midX = (startPoint.x + endPoint.x) / 2;
const midY = (startPoint.y + endPoint.y) / 2;
let controlX = midX;
let controlY = midY;
// 根据方向调整控制点
if (Math.abs(dx) > Math.abs(dy)) {
controlY = midY + (dy > 0 ? -1 : 1) * distance * CURVE_CONFIG.curvature;
} else {
controlX = midX + (dx > 0 ? -1 : 1) * distance * CURVE_CONFIG.curvature;
}
// 确保控制点在容器范围内
controlX = Math.max(CURVE_CONFIG.minControlOffset,
Math.min(containerSize.width - CURVE_CONFIG.minControlOffset, controlX));
controlY = Math.max(CURVE_CONFIG.minControlOffset,
Math.min(containerSize.height - CURVE_CONFIG.minControlOffset, controlY));
// 创建二次贝塞尔曲线路径
return `M ${startPoint.x} ${startPoint.y} Q ${controlX} ${controlY} ${endPoint.x} ${endPoint.y}`;
}
/**
*
*/
export interface ConnectionAnimationConfig {
line: {
initial: Record<string, any>;
animate: Record<string, any>;
transition: Record<string, any>;
};
arrow: {
initial: Record<string, any>;
animate: Record<string, any>;
transition: Record<string, any>;
};
}
/**
*
*/
export function getConnectionAnimationConfig(animated: boolean = true): ConnectionAnimationConfig {
if (!animated) {
return {
line: {
initial: {},
animate: {},
transition: {}
},
arrow: {
initial: {},
animate: {},
transition: {}
}
};
}
return {
line: {
initial: { pathLength: 0, opacity: 0 },
animate: { pathLength: 1, opacity: 1 },
transition: {
pathLength: {
duration: CONNECTION_STYLE.animation.pathDuration,
ease: CONNECTION_STYLE.animation.pathEasing as any
},
opacity: { duration: CONNECTION_STYLE.animation.opacityDuration }
}
},
arrow: {
initial: { scale: 0, opacity: 0 },
animate: { scale: 1, opacity: 1 },
transition: {
delay: CONNECTION_STYLE.animation.arrowDelay,
duration: CONNECTION_STYLE.animation.arrowDuration,
type: "spring" as const,
...CONNECTION_STYLE.animation.springConfig
}
}
};
}

View File

@ -2,7 +2,32 @@
* *
*/ */
import { post } from '@/api/request'; // 注意:这里不使用 @/api/request 中的 post 函数,因为它会将请求发送到远程服务器
// 我们需要直接调用本地的 Next.js API 路由
/**
* API请求函数 - Next.js API路由
*/
const localPost = async <T>(url: string, data: any): Promise<T> => {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Local API request failed:', error);
throw error;
}
};
/** /**
* SSO配置接口 * SSO配置接口
@ -14,6 +39,13 @@ export interface SSOConfig {
description: string; description: string;
} }
/**
*
*/
export interface VideoModificationConfig {
show: boolean;
}
/** /**
* SSO配置 * SSO配置
* @returns Promise<SSOConfig | null> * @returns Promise<SSOConfig | null>
@ -21,7 +53,7 @@ export interface SSOConfig {
export const getSSOConfig = async (): Promise<SSOConfig | null> => { export const getSSOConfig = async (): Promise<SSOConfig | null> => {
try { try {
console.log('🔍 开始获取SSO配置...'); console.log('🔍 开始获取SSO配置...');
const res = await post<any>(`/api/server-setting/find_by_code`, { code: 'sso_config' }); const res = await localPost<any>(`/api/server-setting/find_by_code`, { code: 'sso_config' });
console.log('📋 SSO API响应:', res); console.log('📋 SSO API响应:', res);
@ -86,3 +118,75 @@ export const isGoogleLoginEnabled = async (): Promise<boolean> => {
return false; // 出错时默认不显示 return false; // 出错时默认不显示
} }
}; };
/**
*
* @returns Promise<VideoModificationConfig | null>
*/
export const getVideoModificationConfig = async (): Promise<VideoModificationConfig | null> => {
try {
console.log('🔍 开始获取视频修改配置...');
const res = await localPost<any>(`/api/server-setting/find_by_code`, { code: 'video_modification' });
console.log('📋 视频修改配置API响应:', res);
if (!res || res.code !== 0 || !res.successful || !res.data) {
console.warn('❌ Failed to fetch video modification config:', res);
return null;
}
// 新的数据格式data直接包含id, code, value等字段
const { value } = res.data;
console.log('📝 视频修改配置原始value:', value);
console.log('📝 value类型:', typeof value, 'value长度:', value?.length);
if (typeof value !== 'string' || value.length === 0) {
console.warn('❌ Invalid video modification config format:', value);
return null;
}
try {
const config: VideoModificationConfig = JSON.parse(value);
console.log('✅ 视频修改配置解析成功:', config);
return config;
} catch (parseError) {
console.error('❌ Failed to parse video modification config:', parseError);
console.error('❌ 原始value:', JSON.stringify(value));
return null;
}
} catch (error) {
console.error('❌ Error fetching video modification config:', error);
return null;
}
};
/**
*
* @returns Promise<boolean>
*/
export const isVideoModificationEnabled = async (): Promise<boolean> => {
try {
console.log('🔍 检查视频修改功能是否启用...');
const config = await getVideoModificationConfig();
console.log('📋 获得的视频修改配置:', config);
if (!config) {
console.log('❌ 没有获得视频修改配置返回false');
return false;
}
const isEnabled = config?.show === true;
console.log('🔍 视频修改配置检查:', {
show: config?.show,
isEnabled,
finalResult: isEnabled
});
// 简化逻辑只检查show字段
return isEnabled;
} catch (error) {
console.error('❌ Error checking video modification status:', error);
return false; // 出错时默认不显示
}
};