byteplan-cli 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/package.json +7 -3
  2. package/skills/byteplan-analysis/SKILL.md +1078 -0
  3. package/skills/byteplan-api/API_REFERENCE.md +249 -0
  4. package/skills/byteplan-api/SKILL.md +96 -0
  5. package/skills/byteplan-api/package.json +16 -0
  6. package/skills/byteplan-api/scripts/api.js +973 -0
  7. package/skills/byteplan-excel/SKILL.md +212 -0
  8. package/skills/byteplan-excel/examples/margin-analysis.json +40 -0
  9. package/skills/byteplan-excel/package.json +12 -0
  10. package/skills/byteplan-excel/pnpm-lock.yaml +68 -0
  11. package/skills/byteplan-excel/scripts/generate_excel.js +156 -0
  12. package/skills/byteplan-html/SKILL.md +490 -0
  13. package/skills/byteplan-html/examples/example-output.html +184 -0
  14. package/skills/byteplan-html/examples/generate-ppt-style-html.js +611 -0
  15. package/skills/byteplan-html/examples/margin-contribution-analysis.json +152 -0
  16. package/skills/byteplan-html/package.json +18 -0
  17. package/skills/byteplan-html/scripts/generate_html.js +517 -0
  18. package/skills/byteplan-ppt/SKILL.md +394 -0
  19. package/skills/byteplan-ppt/examples/margin-contribution-analysis.json +152 -0
  20. package/skills/byteplan-ppt/package.json +16 -0
  21. package/skills/byteplan-ppt/pnpm-lock.yaml +138 -0
  22. package/skills/byteplan-ppt/scripts/check_ppt_overlap.js +318 -0
  23. package/skills/byteplan-ppt/scripts/generate_ppt.js +680 -0
  24. package/skills/byteplan-video/SKILL.md +606 -0
  25. package/skills/byteplan-video/examples/sample-video-data.json +82 -0
  26. package/skills/byteplan-video/remotion-project/package.json +22 -0
  27. package/skills/byteplan-video/remotion-project/pnpm-lock.yaml +1646 -0
  28. package/skills/byteplan-video/remotion-project/remotion.config.ts +6 -0
  29. package/skills/byteplan-video/remotion-project/scene_durations.json +32 -0
  30. package/skills/byteplan-video/remotion-project/scripts/generate_audio.js +279 -0
  31. package/skills/byteplan-video/remotion-project/src/DynamicReport.tsx +172 -0
  32. package/skills/byteplan-video/remotion-project/src/Root.tsx +51 -0
  33. package/skills/byteplan-video/remotion-project/src/SalesReport.tsx +107 -0
  34. package/skills/byteplan-video/remotion-project/src/index.tsx +4 -0
  35. package/skills/byteplan-video/remotion-project/src/scenes/ChartSlide.tsx +201 -0
  36. package/skills/byteplan-video/remotion-project/src/scenes/CoverSlide.tsx +61 -0
  37. package/skills/byteplan-video/remotion-project/src/scenes/EndSlide.tsx +60 -0
  38. package/skills/byteplan-video/remotion-project/src/scenes/InsightSlide.tsx +101 -0
  39. package/skills/byteplan-video/remotion-project/src/scenes/KpiSlide.tsx +84 -0
  40. package/skills/byteplan-video/remotion-project/src/scenes/RecommendationSlide.tsx +100 -0
  41. package/skills/byteplan-video/remotion-project/tsconfig.json +13 -0
  42. package/skills/byteplan-video/remotion-project/video_data.json +76 -0
  43. package/skills/byteplan-video/scripts/generate_video.js +270 -0
  44. package/skills/byteplan-video/templates/package.json +31 -0
  45. package/skills/byteplan-video/templates/pnpm-lock.yaml +2200 -0
  46. package/skills/byteplan-video/templates/remotion.config.ts +9 -0
  47. package/skills/byteplan-video/templates/scripts/generate-audio.ts +55 -0
  48. package/skills/byteplan-video/templates/src/components/BarChartScene.tsx +153 -0
  49. package/skills/byteplan-video/templates/src/components/InsightScene.tsx +135 -0
  50. package/skills/byteplan-video/templates/src/components/LineChartScene.tsx +214 -0
  51. package/skills/byteplan-video/templates/src/components/SceneFactory.tsx +34 -0
  52. package/skills/byteplan-video/templates/src/components/SummaryScene.tsx +155 -0
  53. package/skills/byteplan-video/templates/src/components/TitleScene.tsx +130 -0
  54. package/skills/byteplan-video/templates/src/compositions/AnalysisVideo.tsx +39 -0
  55. package/skills/byteplan-video/templates/src/index.tsx +28 -0
  56. package/skills/byteplan-video/templates/src/register-root.tsx +4 -0
  57. package/skills/byteplan-video/templates/src/storyboard/types.ts +46 -0
  58. package/skills/byteplan-video/templates/tsconfig.json +17 -0
  59. package/skills/byteplan-video/templates/tsconfig.scripts.json +13 -0
  60. package/skills/byteplan-word/SKILL.md +233 -0
  61. package/skills/byteplan-word/package.json +12 -0
  62. package/skills/byteplan-word/pnpm-lock.yaml +120 -0
  63. package/skills/byteplan-word/scripts/generate_word.js +548 -0
  64. package/src/cli.js +4 -0
  65. package/src/commands/skills.js +279 -0
@@ -0,0 +1,9 @@
1
+ import { Config } from "@remotion/cli/config";
2
+
3
+ // macOS Chrome 路径
4
+ Config.setBrowserExecutable("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
5
+
6
+ // 设置入口文件
7
+ Config.setEntryPoint("src/register-root.tsx");
8
+
9
+ export default Config;
@@ -0,0 +1,55 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { getAudioDurationInSeconds } from 'get-audio-duration';
5
+
6
+ const VOICE = 'zh-CN-XiaoxiaoNeural'; // 女声,自然流畅
7
+
8
+ async function generateAudio() {
9
+ const storyboardPath = path.join(process.cwd(), 'src/storyboard/scenes.json');
10
+ const audioDir = path.join(process.cwd(), 'public/audio');
11
+
12
+ // 确保音频目录存在
13
+ if (!fs.existsSync(audioDir)) {
14
+ fs.mkdirSync(audioDir, { recursive: true });
15
+ }
16
+
17
+ // 读取分镜配置
18
+ const scenesData = JSON.parse(fs.readFileSync(storyboardPath, 'utf-8'));
19
+ const scenes = Array.isArray(scenesData) ? scenesData : scenesData.scenes;
20
+
21
+ console.log(`开始生成 ${scenes.length} 个场景的语音...\n`);
22
+
23
+ for (const scene of scenes) {
24
+ const audioFileName = `${scene.id}.mp3`;
25
+ const audioPath = path.join(audioDir, audioFileName);
26
+
27
+ console.log(`生成语音: ${scene.id}`);
28
+ console.log(` 文本: ${scene.narration.substring(0, 30)}...`);
29
+
30
+ // 使用 edge-tts CLI 生成语音
31
+ const text = scene.narration.replace(/"/g, '\\"');
32
+ const cmd = `edge-tts --voice "${VOICE}" --text "${text}" --write-media "${audioPath}"`;
33
+
34
+ try {
35
+ execSync(cmd, { stdio: 'pipe' });
36
+ } catch (error) {
37
+ console.error(` 错误: ${error}`);
38
+ continue;
39
+ }
40
+
41
+ // 获取音频时长
42
+ const duration = await getAudioDurationInSeconds(audioPath);
43
+ scene.audioPath = `/audio/${audioFileName}`;
44
+ scene.audioDuration = Math.ceil(duration);
45
+
46
+ console.log(` 时长: ${duration.toFixed(2)}秒\n`);
47
+ }
48
+
49
+ // 保存更新后的分镜配置
50
+ const outputData = Array.isArray(scenesData) ? scenes : { scenes };
51
+ fs.writeFileSync(storyboardPath, JSON.stringify(outputData, null, 2));
52
+ console.log('语音生成完成!分镜配置已更新。');
53
+ }
54
+
55
+ generateAudio().catch(console.error);
@@ -0,0 +1,153 @@
1
+ import React from "react";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring } from "remotion";
3
+ import { VisualConfig } from "../storyboard/types";
4
+ import chartData from "../data/chartData.json";
5
+
6
+ interface BarChartSceneProps {
7
+ config: VisualConfig;
8
+ narration: string;
9
+ }
10
+
11
+ const COLORS = {
12
+ primary: "#667eea", // BytePlan 紫
13
+ secondary: "#1E293B",
14
+ accent: "#764ba2",
15
+ background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
16
+ text: "#FFFFFF",
17
+ lightText: "#94A3B8",
18
+ grid: "rgba(255,255,255,0.1)",
19
+ };
20
+
21
+ export const BarChartScene: React.FC<BarChartSceneProps> = ({ config }) => {
22
+ const frame = useCurrentFrame();
23
+ const { fps } = useVideoConfig();
24
+
25
+ // 必须提供 dataKey
26
+ if (!config.dataKey) {
27
+ throw new Error("BarChartScene requires 'dataKey' in config");
28
+ }
29
+
30
+ // 获取数据
31
+ const dataKey = config.dataKey;
32
+ const rawData = (chartData as Record<string, unknown>)[dataKey];
33
+ const data = Array.isArray(rawData) ? rawData : [];
34
+
35
+ if (data.length === 0) {
36
+ throw new Error(`BarChartScene: No data found for dataKey '${dataKey}'`);
37
+ }
38
+
39
+ // 动画
40
+ const titleOpacity = spring({ frame, fps, config: { damping: 100 } });
41
+ const chartOpacity = spring({ frame: frame - 20, fps, config: { damping: 100 } });
42
+
43
+ // 计算最大值
44
+ const maxValue = Math.max(...data.map((d: Record<string, number>) => d.value || 0), 1);
45
+ const barWidth = 60;
46
+ const barGap = 30;
47
+ const chartHeight = 350;
48
+ const chartStartX = (1920 - (data.length * (barWidth + barGap) - barGap)) / 2;
49
+
50
+ return (
51
+ <AbsoluteFill style={{ background: COLORS.background }}>
52
+ {/* 标题 */}
53
+ <div style={{
54
+ position: "absolute",
55
+ top: 80,
56
+ left: 0,
57
+ right: 0,
58
+ textAlign: "center",
59
+ opacity: titleOpacity,
60
+ }}>
61
+ <h2 style={{
62
+ fontSize: 48,
63
+ fontWeight: 600,
64
+ color: COLORS.text,
65
+ margin: 0,
66
+ }}>
67
+ {config.title}
68
+ </h2>
69
+ </div>
70
+
71
+ {/* 图表区域 */}
72
+ <div style={{
73
+ position: "absolute",
74
+ top: "50%",
75
+ left: 0,
76
+ right: 0,
77
+ transform: "translateY(-50%)",
78
+ opacity: chartOpacity,
79
+ }}>
80
+ <svg width="1920" height="450" viewBox="0 0 1920 450">
81
+ {/* 网格线 */}
82
+ {[0, 1, 2, 3, 4].map((i) => (
83
+ <line
84
+ key={i}
85
+ x1={chartStartX - 50}
86
+ y1={50 + i * 100}
87
+ x2={1920 - chartStartX + 50}
88
+ y2={50 + i * 100}
89
+ stroke={COLORS.grid}
90
+ strokeWidth="1"
91
+ />
92
+ ))}
93
+
94
+ {/* 柱状图 */}
95
+ {data.map((item: Record<string, unknown>, index: number) => {
96
+ const value = (item.value as number) || 0;
97
+ const label = (item.label as string) || "";
98
+ const height = (value / maxValue) * chartHeight;
99
+ const delay = index * 5;
100
+ const barProgress = spring({ frame: frame - 20 - delay, fps, config: { damping: 100 } });
101
+ const animatedHeight = height * barProgress;
102
+
103
+ return (
104
+ <g key={index}>
105
+ {/* 柱子 */}
106
+ <rect
107
+ x={chartStartX + index * (barWidth + barGap)}
108
+ y={400 - animatedHeight}
109
+ width={barWidth}
110
+ height={animatedHeight}
111
+ fill={`url(#barGradient${index})`}
112
+ rx={8}
113
+ />
114
+
115
+ {/* 数值标签 */}
116
+ <text
117
+ x={chartStartX + index * (barWidth + barGap) + barWidth / 2}
118
+ y={400 - animatedHeight - 15}
119
+ textAnchor="middle"
120
+ fill={COLORS.text}
121
+ fontSize={20}
122
+ fontWeight="600"
123
+ opacity={barProgress}
124
+ >
125
+ {typeof value === 'number' ? (value >= 100 ? `${(value / 100).toFixed(0)}亿` : `${value}%`) : value}
126
+ </text>
127
+
128
+ {/* X轴标签 */}
129
+ <text
130
+ x={chartStartX + index * (barWidth + barGap) + barWidth / 2}
131
+ y={430}
132
+ textAnchor="middle"
133
+ fill={COLORS.lightText}
134
+ fontSize={18}
135
+ >
136
+ {label}
137
+ </text>
138
+
139
+ {/* 渐变定义 */}
140
+ <defs>
141
+ <linearGradient id={`barGradient${index}`} x1="0%" y1="0%" x2="0%" y2="100%">
142
+ <stop offset="0%" stopColor={COLORS.primary} />
143
+ <stop offset="100%" stopColor={COLORS.accent} />
144
+ </linearGradient>
145
+ </defs>
146
+ </g>
147
+ );
148
+ })}
149
+ </svg>
150
+ </div>
151
+ </AbsoluteFill>
152
+ );
153
+ };
@@ -0,0 +1,135 @@
1
+ import React from "react";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring } from "remotion";
3
+ import { VisualConfig } from "../storyboard/types";
4
+
5
+ interface InsightSceneProps {
6
+ config: VisualConfig;
7
+ narration: string;
8
+ }
9
+
10
+ const COLORS = {
11
+ primary: "#667eea", // BytePlan 紫
12
+ secondary: "#1E293B",
13
+ accent: "#764ba2",
14
+ background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
15
+ text: "#FFFFFF",
16
+ lightText: "#94A3B8",
17
+ highlight: "#22C55E",
18
+ };
19
+
20
+ export const InsightScene: React.FC<InsightSceneProps> = ({ config }) => {
21
+ const frame = useCurrentFrame();
22
+ const { fps } = useVideoConfig();
23
+
24
+ // 必须提供 highlights 数据
25
+ const highlights = config.highlights;
26
+
27
+ if (!highlights || highlights.length === 0) {
28
+ throw new Error("InsightScene requires 'highlights' array in config");
29
+ }
30
+
31
+ // 动画
32
+ const titleOpacity = spring({ frame, fps, config: { damping: 100 } });
33
+ const contentOpacity = spring({ frame: frame - 15, fps, config: { damping: 100 } });
34
+
35
+ return (
36
+ <AbsoluteFill style={{ background: COLORS.background }}>
37
+ {/* 装饰元素 */}
38
+ <div style={{
39
+ position: "absolute",
40
+ top: "20%",
41
+ right: "10%",
42
+ width: 150,
43
+ height: 150,
44
+ borderRadius: "50%",
45
+ background: "radial-gradient(circle, rgba(255,106,0,0.2) 0%, transparent 70%)",
46
+ filter: "blur(30px)",
47
+ }} />
48
+
49
+ {/* 标题 */}
50
+ <div style={{
51
+ position: "absolute",
52
+ top: 100,
53
+ left: 0,
54
+ right: 0,
55
+ textAlign: "center",
56
+ opacity: titleOpacity,
57
+ }}>
58
+ <h2 style={{
59
+ fontSize: 56,
60
+ fontWeight: 700,
61
+ color: COLORS.text,
62
+ margin: 0,
63
+ }}>
64
+ {config.title}
65
+ </h2>
66
+ <div style={{
67
+ width: 120,
68
+ height: 4,
69
+ background: COLORS.primary,
70
+ margin: "20px auto",
71
+ }} />
72
+ </div>
73
+
74
+ {/* 洞察要点 */}
75
+ <div style={{
76
+ position: "absolute",
77
+ top: 250,
78
+ left: 150,
79
+ right: 150,
80
+ display: "flex",
81
+ flexDirection: "column",
82
+ gap: 30,
83
+ opacity: contentOpacity,
84
+ }}>
85
+ {highlights.map((point, index) => {
86
+ const delay = index * 8;
87
+ const itemOpacity = spring({ frame: frame - 25 - delay, fps, config: { damping: 100 } });
88
+ const itemX = spring({ frame: frame - 25 - delay, fps, config: { damping: 100, stiffness: 80 } });
89
+
90
+ return (
91
+ <div
92
+ key={index}
93
+ style={{
94
+ display: "flex",
95
+ alignItems: "center",
96
+ gap: 24,
97
+ opacity: itemOpacity,
98
+ transform: `translateX(${(1 - itemX) * -50}px)`,
99
+ }}
100
+ >
101
+ {/* 图标 */}
102
+ <div style={{
103
+ width: 48,
104
+ height: 48,
105
+ borderRadius: 12,
106
+ background: `linear-gradient(135deg, ${COLORS.primary}, ${COLORS.accent})`,
107
+ display: "flex",
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ fontSize: 24,
111
+ flexShrink: 0,
112
+ }}>
113
+
114
+ </div>
115
+
116
+ {/* 文本 */}
117
+ <div style={{
118
+ fontSize: 32,
119
+ fontWeight: 500,
120
+ color: COLORS.text,
121
+ padding: "16px 32px",
122
+ background: "rgba(255,255,255,0.05)",
123
+ borderRadius: 12,
124
+ borderLeft: `4px solid ${COLORS.primary}`,
125
+ flex: 1,
126
+ }}>
127
+ {point}
128
+ </div>
129
+ </div>
130
+ );
131
+ })}
132
+ </div>
133
+ </AbsoluteFill>
134
+ );
135
+ };
@@ -0,0 +1,214 @@
1
+ import React from "react";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring } from "remotion";
3
+ import { VisualConfig } from "../storyboard/types";
4
+ import chartData from "../data/chartData.json";
5
+
6
+ interface LineChartSceneProps {
7
+ config: VisualConfig;
8
+ narration: string;
9
+ }
10
+
11
+ const COLORS = {
12
+ primary: "#667eea", // BytePlan 紫
13
+ secondary: "#1E293B",
14
+ accent: "#764ba2",
15
+ background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
16
+ text: "#FFFFFF",
17
+ lightText: "#94A3B8",
18
+ grid: "rgba(255,255,255,0.1)",
19
+ };
20
+
21
+ export const LineChartScene: React.FC<LineChartSceneProps> = ({ config }) => {
22
+ const frame = useCurrentFrame();
23
+ const { fps } = useVideoConfig();
24
+
25
+ // 必须提供 dataKey
26
+ if (!config.dataKey) {
27
+ throw new Error("LineChartScene requires 'dataKey' in config");
28
+ }
29
+
30
+ // 获取数据
31
+ const dataKey = config.dataKey;
32
+ const rawData = (chartData as Record<string, unknown>)[dataKey];
33
+ const data = Array.isArray(rawData) ? rawData : [];
34
+
35
+ if (data.length === 0) {
36
+ throw new Error(`LineChartScene: No data found for dataKey '${dataKey}'`);
37
+ }
38
+
39
+ // 动画
40
+ const titleOpacity = spring({ frame, fps, config: { damping: 100 } });
41
+ const chartOpacity = spring({ frame: frame - 20, fps, config: { damping: 100 } });
42
+ const lineProgress = spring({ frame: frame - 30, fps, config: { damping: 100, stiffness: 50 } });
43
+
44
+ // 计算坐标
45
+ const chartWidth = 1400;
46
+ const chartHeight = 350;
47
+ const chartStartX = 260;
48
+ const chartStartY = 80;
49
+
50
+ const maxValue = Math.max(...data.map((d: Record<string, number>) => d.value || 0), 1);
51
+ const pointSpacing = chartWidth / (data.length - 1 || 1);
52
+
53
+ // 生成路径
54
+ const generatePath = () => {
55
+ if (data.length === 0) return "";
56
+
57
+ const points = data.map((item: Record<string, unknown>, index: number) => {
58
+ const value = (item.value as number) || 0;
59
+ const x = chartStartX + index * pointSpacing;
60
+ const y = chartStartY + chartHeight - (value / maxValue) * chartHeight;
61
+ return { x, y, value };
62
+ });
63
+
64
+ let path = `M ${points[0].x} ${points[0].y}`;
65
+
66
+ for (let i = 1; i < points.length; i++) {
67
+ const prev = points[i - 1];
68
+ const curr = points[i];
69
+ const midX = (prev.x + curr.x) / 2;
70
+ path += ` C ${midX} ${prev.y}, ${midX} ${curr.y}, ${curr.x} ${curr.y}`;
71
+ }
72
+
73
+ return path;
74
+ };
75
+
76
+ const pathLength = 3000; // 估计的路径长度
77
+ const animatedDashoffset = pathLength * (1 - lineProgress);
78
+
79
+ return (
80
+ <AbsoluteFill style={{ background: COLORS.background }}>
81
+ {/* 标题 */}
82
+ <div style={{
83
+ position: "absolute",
84
+ top: 60,
85
+ left: 0,
86
+ right: 0,
87
+ textAlign: "center",
88
+ opacity: titleOpacity,
89
+ }}>
90
+ <h2 style={{
91
+ fontSize: 48,
92
+ fontWeight: 600,
93
+ color: COLORS.text,
94
+ margin: 0,
95
+ }}>
96
+ {config.title}
97
+ </h2>
98
+ </div>
99
+
100
+ {/* 图表区域 */}
101
+ <div style={{
102
+ position: "absolute",
103
+ top: 150,
104
+ left: 0,
105
+ right: 0,
106
+ opacity: chartOpacity,
107
+ }}>
108
+ <svg width="1920" height="500" viewBox="0 0 1920 500">
109
+ {/* 网格线 */}
110
+ {[0, 1, 2, 3, 4].map((i) => (
111
+ <g key={i}>
112
+ <line
113
+ x1={chartStartX}
114
+ y1={chartStartY + i * (chartHeight / 4)}
115
+ x2={chartStartX + chartWidth}
116
+ y2={chartStartY + i * (chartHeight / 4)}
117
+ stroke={COLORS.grid}
118
+ strokeWidth="1"
119
+ />
120
+ {/* Y轴标签 */}
121
+ <text
122
+ x={chartStartX - 30}
123
+ y={chartStartY + i * (chartHeight / 4) + 5}
124
+ textAnchor="end"
125
+ fill={COLORS.lightText}
126
+ fontSize={16}
127
+ >
128
+ {Math.round(maxValue - (maxValue / 4) * i)}亿
129
+ </text>
130
+ </g>
131
+ ))}
132
+
133
+ {/* 渐变填充区域 */}
134
+ <defs>
135
+ <linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
136
+ <stop offset="0%" stopColor={COLORS.primary} stopOpacity="0.4" />
137
+ <stop offset="100%" stopColor={COLORS.primary} stopOpacity="0" />
138
+ </linearGradient>
139
+ </defs>
140
+
141
+ {/* 填充区域 */}
142
+ {data.length > 0 && (
143
+ <path
144
+ d={`${generatePath()} L ${chartStartX + (data.length - 1) * pointSpacing} ${chartStartY + chartHeight} L ${chartStartX} ${chartStartY + chartHeight} Z`}
145
+ fill="url(#areaGradient)"
146
+ opacity={lineProgress}
147
+ />
148
+ )}
149
+
150
+ {/* 折线 */}
151
+ <path
152
+ d={generatePath()}
153
+ fill="none"
154
+ stroke={COLORS.primary}
155
+ strokeWidth="4"
156
+ strokeLinecap="round"
157
+ strokeLinejoin="round"
158
+ strokeDasharray={pathLength}
159
+ strokeDashoffset={animatedDashoffset}
160
+ />
161
+
162
+ {/* 数据点 */}
163
+ {data.map((item: Record<string, unknown>, index: number) => {
164
+ const value = (item.value as number) || 0;
165
+ const label = (item.label as string) || "";
166
+ const x = chartStartX + index * pointSpacing;
167
+ const y = chartStartY + chartHeight - (value / maxValue) * chartHeight;
168
+ const delay = index * 3;
169
+ const pointOpacity = spring({ frame: frame - 40 - delay, fps, config: { damping: 100 } });
170
+
171
+ return (
172
+ <g key={index}>
173
+ {/* 数据点 */}
174
+ <circle
175
+ cx={x}
176
+ cy={y}
177
+ r={10}
178
+ fill={COLORS.primary}
179
+ stroke={COLORS.text}
180
+ strokeWidth="3"
181
+ opacity={pointOpacity}
182
+ />
183
+
184
+ {/* 数值 */}
185
+ <text
186
+ x={x}
187
+ y={y - 25}
188
+ textAnchor="middle"
189
+ fill={COLORS.text}
190
+ fontSize={20}
191
+ fontWeight="600"
192
+ opacity={pointOpacity}
193
+ >
194
+ {(value / 100).toFixed(0)}亿
195
+ </text>
196
+
197
+ {/* X轴标签 */}
198
+ <text
199
+ x={x}
200
+ y={chartStartY + chartHeight + 40}
201
+ textAnchor="middle"
202
+ fill={COLORS.lightText}
203
+ fontSize={18}
204
+ >
205
+ {label}
206
+ </text>
207
+ </g>
208
+ );
209
+ })}
210
+ </svg>
211
+ </div>
212
+ </AbsoluteFill>
213
+ );
214
+ };
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ import { StoryboardScene } from "../storyboard/types";
3
+ import { TitleScene } from "./TitleScene";
4
+ import { BarChartScene } from "./BarChartScene";
5
+ import { LineChartScene } from "./LineChartScene";
6
+ import { InsightScene } from "./InsightScene";
7
+ import { SummaryScene } from "./SummaryScene";
8
+
9
+ const sceneComponents: Record<string, React.FC<{ config: any; narration: string }>> = {
10
+ title: TitleScene,
11
+ bar_chart: BarChartScene,
12
+ line_chart: LineChartScene,
13
+ insight: InsightScene,
14
+ summary: SummaryScene,
15
+ };
16
+
17
+ export const SceneFactory: React.FC<{ scene: any }> = ({ scene }) => {
18
+ const Component = sceneComponents[scene.type];
19
+ if (!Component) {
20
+ return (
21
+ <div style={{ padding: 40, color: "white", background: "#1a1a2e" }}>
22
+ Unknown scene type: {scene.type}
23
+ </div>
24
+ );
25
+ }
26
+ // 支持 visualConfig 或直接使用 scene 作为 config
27
+ const config = scene.visualConfig || {
28
+ title: scene.title,
29
+ subtitle: scene.subtitle,
30
+ data: scene.data,
31
+ content: scene.content,
32
+ };
33
+ return <Component config={config} narration={scene.narration} />;
34
+ };