update:1.新增AI生成组织功能,扩展优化组织字段(所在地 代表颜色 格言/口号)

2.适配移动端项目管理-剧情分析UI页面
This commit is contained in:
xiamuceer
2025-11-05 16:22:14 +08:00
parent ff58548a79
commit 397ca30bcb
17 changed files with 1155 additions and 185 deletions
@@ -32,6 +32,7 @@ interface AnnotatedTextProps {
onAnnotationClick?: (annotation: MemoryAnnotation) => void;
activeAnnotationId?: string;
scrollToAnnotation?: string;
style?: React.CSSProperties;
}
// 类型颜色映射
@@ -60,6 +61,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
onAnnotationClick,
activeAnnotationId,
scrollToAnnotation,
style,
}) => {
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({});
@@ -243,6 +245,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
fontSize: 16,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
...style,
}}
>
{segments.map((segment, index) => renderAnnotatedSegment(segment, index))}
+50 -30
View File
@@ -14,6 +14,9 @@ import {
} from '@ant-design/icons';
import type { AnalysisTask, ChapterAnalysisResponse } from '../types';
// 判断是否为移动设备
const isMobileDevice = () => window.innerWidth < 768;
interface ChapterAnalysisProps {
chapterId: string;
visible: boolean;
@@ -25,14 +28,23 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isMobile, setIsMobile] = useState(isMobileDevice());
useEffect(() => {
if (visible && chapterId) {
fetchAnalysisStatus();
}
// 监听窗口大小变化
const handleResize = () => {
setIsMobile(isMobileDevice());
};
window.addEventListener('resize', handleResize);
// 清理函数:组件卸载或关闭时清除轮询
return () => {
window.removeEventListener('resize', handleResize);
// 清除可能存在的轮询
};
}, [visible, chapterId]);
@@ -194,10 +206,10 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: '概览',
icon: <TrophyOutlined />,
children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card title="整体评分" style={{ marginBottom: 16 }}>
<Row gutter={16}>
<Col span={6}>
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card title="整体评分" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<Row gutter={isMobile ? 8 : 16}>
<Col span={isMobile ? 12 : 6}>
<Statistic
title="整体质量"
value={analysis_data.overall_quality_score || 0}
@@ -205,21 +217,21 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
valueStyle={{ color: '#3f8600' }}
/>
</Col>
<Col span={6}>
<Col span={isMobile ? 12 : 6}>
<Statistic
title="节奏把控"
value={analysis_data.pacing_score || 0}
suffix="/ 10"
/>
</Col>
<Col span={6}>
<Col span={isMobile ? 12 : 6}>
<Statistic
title="吸引力"
value={analysis_data.engagement_score || 0}
suffix="/ 10"
/>
</Col>
<Col span={6}>
<Col span={isMobile ? 12 : 6}>
<Statistic
title="连贯性"
value={analysis_data.coherence_score || 0}
@@ -230,15 +242,15 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
</Card>
{analysis_data.analysis_report && (
<Card title="分析摘要" style={{ marginBottom: 16 }}>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
<Card title="分析摘要" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', fontSize: isMobile ? 13 : 14 }}>
{analysis_data.analysis_report}
</pre>
</Card>
)}
{analysis_data.suggestions && analysis_data.suggestions.length > 0 && (
<Card title={<><BulbOutlined /> </>}>
<Card title={<><BulbOutlined /> </>} size={isMobile ? 'small' : 'default'}>
<List
dataSource={analysis_data.suggestions}
renderItem={(item, index) => (
@@ -257,8 +269,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `钩子 (${analysis_data.hooks?.length || 0})`,
icon: <ThunderboltOutlined />,
children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card>
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.hooks && analysis_data.hooks.length > 0 ? (
<List
dataSource={analysis_data.hooks}
@@ -289,8 +301,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `伏笔 (${analysis_data.foreshadows?.length || 0})`,
icon: <FireOutlined />,
children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card>
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.foreshadows && analysis_data.foreshadows.length > 0 ? (
<List
dataSource={analysis_data.foreshadows}
@@ -326,18 +338,18 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: '情感曲线',
icon: <HeartOutlined />,
children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card>
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.emotional_tone ? (
<div>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={12}>
<Row gutter={isMobile ? 8 : 16} style={{ marginBottom: isMobile ? 16 : 24 }}>
<Col span={isMobile ? 24 : 12}>
<Statistic
title="主导情绪"
value={analysis_data.emotional_tone}
/>
</Col>
<Col span={12}>
<Col span={isMobile ? 24 : 12}>
<Statistic
title="情感强度"
value={(analysis_data.emotional_intensity * 10).toFixed(1)}
@@ -372,8 +384,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `角色 (${analysis_data.character_states?.length || 0})`,
icon: <TeamOutlined />,
children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card>
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.character_states && analysis_data.character_states.length > 0 ? (
<List
dataSource={analysis_data.character_states}
@@ -414,8 +426,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `记忆 (${memories?.length || 0})`,
icon: <FireOutlined />,
children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card>
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{memories && memories.length > 0 ? (
<List
dataSource={memories}
@@ -462,20 +474,25 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
title="章节分析"
open={visible}
onCancel={onClose}
width="90%"
centered
width={isMobile ? '100%' : '90%'}
centered={!isMobile}
style={{
maxWidth: '1400px',
paddingBottom: 0
maxWidth: isMobile ? '100%' : '1400px',
paddingBottom: 0,
top: isMobile ? 0 : undefined,
margin: isMobile ? 0 : undefined,
maxHeight: isMobile ? '100vh' : undefined
}}
styles={{
body: {
padding: '24px',
paddingBottom: 0
padding: isMobile ? '12px' : '24px',
paddingBottom: 0,
maxHeight: isMobile ? 'calc(100vh - 110px)' : undefined,
overflowY: isMobile ? 'auto' : undefined
}
}}
footer={[
<Button key="close" onClick={onClose}>
<Button key="close" onClick={onClose} size={isMobile ? 'small' : 'middle'}>
</Button>,
!task && !loading && (
@@ -485,6 +502,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
icon={<ReloadOutlined />}
onClick={triggerAnalysis}
loading={loading}
size={isMobile ? 'small' : 'middle'}
>
</Button>
@@ -497,6 +515,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
onClick={triggerAnalysis}
loading={loading}
danger
size={isMobile ? 'small' : 'middle'}
>
</Button>
@@ -508,6 +527,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
icon={<ReloadOutlined />}
onClick={triggerAnalysis}
loading={loading}
size={isMobile ? 'small' : 'middle'}
>
</Button>
+36
View File
@@ -122,6 +122,42 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
<Tag color="cyan">{character.organization_type}</Tag>
</div>
)}
{character.power_level !== undefined && character.power_level !== null && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Tag color={character.power_level >= 70 ? 'red' : character.power_level >= 50 ? 'orange' : 'default'}>
{character.power_level}
</Tag>
</div>
)}
{character.location && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Text
style={{ flex: 1, minWidth: 0 }}
ellipsis={{ tooltip: character.location }}
>
{character.location}
</Text>
</div>
)}
{character.color && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Text style={{ flex: 1, minWidth: 0 }}>{character.color}</Text>
</div>
)}
{character.motto && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Text
style={{ flex: 1, minWidth: 0 }}
ellipsis={{ tooltip: character.motto }}
>
{character.motto}
</Text>
</div>
)}
{character.organization_purpose && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
+257 -93
View File
@@ -6,6 +6,7 @@ import {
MenuOutlined,
LeftOutlined,
RightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import { useParams } from 'react-router-dom';
import api from '../services/api';
@@ -71,8 +72,20 @@ const ChapterAnalysis: React.FC = () => {
const [showAnnotations, setShowAnnotations] = useState(true);
const [activeAnnotationId, setActiveAnnotationId] = useState<string | undefined>();
const [sidebarVisible, setSidebarVisible] = useState(false);
const [chapterListVisible, setChapterListVisible] = useState(false);
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
// 监听窗口大小变化
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 加载章节列表
useEffect(() => {
@@ -128,6 +141,9 @@ const ChapterAnalysis: React.FC = () => {
const handleChapterSelect = (chapterId: string) => {
loadChapterContent(chapterId);
if (isMobile) {
setChapterListVisible(false);
}
};
const handlePreviousChapter = () => {
@@ -151,7 +167,7 @@ const ChapterAnalysis: React.FC = () => {
// 清除滚动状态
setTimeout(() => setScrollToSidebarAnnotation(undefined), 100);
if (window.innerWidth < 768) {
if (isMobile) {
setSidebarVisible(true);
}
} else {
@@ -173,48 +189,102 @@ const ChapterAnalysis: React.FC = () => {
}
return (
<div style={{ display: 'flex', height: '100%', gap: 16 }}>
{/* 左侧章节列表 */}
<Card
title="章节列表"
style={{ width: 280, height: '100%', overflow: 'hidden' }}
bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => handleChapterSelect(chapter.id)}
style={{
cursor: 'pointer',
padding: '12px 16px',
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Card>
<div style={{
display: 'flex',
height: '100%',
gap: isMobile ? 0 : 16,
flexDirection: isMobile ? 'column' : 'row'
}}>
{/* 左侧章节列表 - 桌面端 */}
{!isMobile && (
<Card
title="章节列表"
style={{ width: 280, height: '100%', overflow: 'hidden' }}
bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => handleChapterSelect(chapter.id)}
style={{
cursor: 'pointer',
padding: '12px 16px',
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Card>
)}
{/* 移动端章节列表抽屉 */}
{isMobile && (
<Drawer
title="章节列表"
placement="left"
onClose={() => setChapterListVisible(false)}
open={chapterListVisible}
width="85%"
styles={{ body: { padding: 0 } }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => handleChapterSelect(chapter.id)}
style={{
cursor: 'pointer',
padding: '12px 16px',
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Drawer>
)}
{/* 右侧内容区域 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
@@ -225,54 +295,138 @@ const ChapterAnalysis: React.FC = () => {
) : (
<>
{/* 工具栏 */}
<Card size="small" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
>
</Button>
<span style={{ fontSize: 16, fontWeight: 600 }}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
>
</Button>
</Space>
<Card size="small" style={{ marginBottom: isMobile ? 8 : 16 }}>
{isMobile ? (
// 移动端布局:两行显示
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 第一行:标题和翻页按钮 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8
}}>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
size="small"
/>
<span style={{
fontSize: 14,
fontWeight: 600,
flex: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0 8px'
}}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
size="small"
/>
</div>
<Space>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
/>
<span style={{ fontSize: 13, color: '#666' }}></span>
<Button
icon={<MenuOutlined />}
onClick={() => setSidebarVisible(true)}
style={{ display: window.innerWidth < 768 ? 'inline-block' : 'none' }}
>
</Button>
</>
)}
</Space>
</div>
{/* 第二行:章节、开关、分析按钮 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8
}}>
<Button
icon={<UnorderedListOutlined />}
onClick={() => setChapterListVisible(true)}
size="small"
>
</Button>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
size="small"
style={{
flexShrink: 0,
height: 16,
minHeight: 16,
lineHeight: '16px'
}}
/>
<Button
icon={<MenuOutlined />}
onClick={() => setSidebarVisible(true)}
size="small"
>
</Button>
</>
)}
</div>
</div>
) : (
// 桌面端布局:保持原样
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Space>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
>
</Button>
<span style={{ fontSize: 16, fontWeight: 600 }}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
>
</Button>
</Space>
<Space>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
/>
<span style={{ fontSize: 13, color: '#666' }}></span>
</>
)}
</Space>
</div>
)}
{hasAnnotations && annotationsData && (
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
<div style={{
marginTop: 12,
fontSize: isMobile ? 11 : 12,
color: '#999',
lineHeight: 1.5
}}>
{annotationsData.summary.total_annotations}
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
{annotationsData.summary.foreshadows > 0 &&
@@ -286,10 +440,16 @@ const ChapterAnalysis: React.FC = () => {
</Card>
{/* 内容区域 */}
<div style={{ flex: 1, display: 'flex', gap: 16, overflow: 'hidden' }}>
<div style={{
flex: 1,
display: 'flex',
gap: isMobile ? 0 : 16,
overflow: 'hidden'
}}>
{/* 章节内容 */}
<Card
style={{ flex: 1, overflow: 'auto' }}
bodyStyle={{ padding: isMobile ? '12px' : '24px' }}
loading={contentLoading}
>
{!contentLoading && (
@@ -311,12 +471,16 @@ const ChapterAnalysis: React.FC = () => {
onAnnotationClick={(annotation) => handleAnnotationClick(annotation, 'content')}
activeAnnotationId={activeAnnotationId}
scrollToAnnotation={scrollToContentAnnotation}
style={{
lineHeight: isMobile ? 1.8 : 2,
fontSize: isMobile ? 14 : 16,
}}
/>
) : (
<div
style={{
lineHeight: 2,
fontSize: 16,
lineHeight: isMobile ? 1.8 : 2,
fontSize: isMobile ? 14 : 16,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
@@ -329,7 +493,7 @@ const ChapterAnalysis: React.FC = () => {
</Card>
{/* 右侧记忆侧边栏(桌面端) */}
{hasAnnotations && annotationsData && window.innerWidth >= 768 && (
{hasAnnotations && annotationsData && !isMobile && (
<Card
style={{ width: 400, overflow: 'auto' }}
bodyStyle={{ padding: 0 }}
@@ -351,7 +515,7 @@ const ChapterAnalysis: React.FC = () => {
placement="right"
onClose={() => setSidebarVisible(false)}
open={sidebarVisible}
width="80%"
width={isMobile ? '90%' : '80%'}
>
<MemorySidebar
annotations={annotationsData.annotations}
+17
View File
@@ -425,6 +425,23 @@ export default function Characters() {
>
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="所在地" name="location">
<Input placeholder="组织的主要活动区域或总部位置" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="代表颜色" name="color">
<Input placeholder="如:深红色、金色、黑色等" />
</Form.Item>
</Col>
</Row>
<Form.Item label="格言/口号" name="motto">
<Input placeholder="组织的宗旨、格言或口号" />
</Form.Item>
</>
)}
+191 -6
View File
@@ -1,10 +1,12 @@
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions } from 'antd';
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import axios from 'axios';
const { TextArea } = Input;
interface Organization {
id: string;
character_id: string;
@@ -15,6 +17,7 @@ interface Organization {
power_level: number;
location?: string;
motto?: string;
color?: string;
}
interface OrganizationMember {
@@ -44,7 +47,11 @@ export default function Organizations() {
const [characters, setCharacters] = useState<Character[]>([]);
const [loading, setLoading] = useState(false);
const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
const [isEditOrgModalOpen, setIsEditOrgModalOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [form] = Form.useForm();
const [editOrgForm] = Form.useForm();
const [generateForm] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => {
@@ -144,6 +151,68 @@ export default function Organizations() {
});
};
const handleGenerateOrganization = async (values: {
name?: string;
organization_type?: string;
background?: string;
requirements?: string;
}) => {
try {
setIsGenerating(true);
await axios.post('/api/organizations/generate', {
project_id: projectId,
name: values.name,
organization_type: values.organization_type,
background: values.background,
requirements: values.requirements,
});
message.success('AI生成组织成功');
Modal.destroyAll();
generateForm.resetFields();
loadOrganizations();
} catch (error: any) {
message.error(error.response?.data?.detail || 'AI生成失败');
} finally {
setIsGenerating(false);
}
};
const showGenerateModal = () => {
Modal.confirm({
title: 'AI生成组织',
width: 600,
centered: !isMobile,
content: (
<Form form={generateForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
label="组织名称"
name="name"
>
<Input placeholder="如:天剑门、黑龙会(可选,AI会自动生成)" />
</Form.Item>
<Form.Item
label="组织类型"
name="organization_type"
>
<Input placeholder="如:门派、帮派、公司、学院(可选,AI会根据世界观生成)" />
</Form.Item>
<Form.Item label="背景设定" name="background">
<TextArea rows={3} placeholder="简要描述组织的背景和环境..." />
</Form.Item>
<Form.Item label="其他要求" name="requirements">
<TextArea rows={2} placeholder="其他特殊要求..." />
</Form.Item>
</Form>
),
okText: '生成',
cancelText: '取消',
onOk: async () => {
const values = await generateForm.validateFields();
await handleGenerateOrganization(values);
},
});
};
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
active: 'green',
@@ -263,6 +332,17 @@ export default function Organizations() {
{!isMobile && <Tag color="blue">{currentProject?.title}</Tag>}
</Space>
}
extra={
<Button
type="dashed"
icon={<ThunderboltOutlined />}
onClick={showGenerateModal}
loading={isGenerating}
size={isMobile ? 'small' : 'middle'}
>
AI生成组织
</Button>
}
>
<div style={{
display: isMobile ? 'flex' : 'grid',
@@ -308,19 +388,53 @@ export default function Organizations() {
<div style={{ minHeight: isMobile ? 'auto' : undefined }}>
{selectedOrg ? (
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Card title="组织详情" size="small">
<Card
title="组织详情"
size="small"
extra={
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => {
editOrgForm.setFieldsValue({
power_level: selectedOrg.power_level,
location: selectedOrg.location,
motto: selectedOrg.motto,
color: selectedOrg.color
});
setIsEditOrgModalOpen(true);
}}
>
</Button>
}
>
<Descriptions column={isMobile ? 1 : 2} size="small">
<Descriptions.Item label="组织名称">{selectedOrg.name}</Descriptions.Item>
<Descriptions.Item label="类型">{selectedOrg.type}</Descriptions.Item>
<Descriptions.Item label="成员数量">{selectedOrg.member_count}</Descriptions.Item>
<Descriptions.Item label="势力等级">{selectedOrg.power_level}</Descriptions.Item>
<Descriptions.Item label="势力等级">
<Tag color={selectedOrg.power_level >= 70 ? 'red' : selectedOrg.power_level >= 50 ? 'orange' : 'default'}>
{selectedOrg.power_level}
</Tag>
</Descriptions.Item>
{selectedOrg.location && (
<Descriptions.Item label="所在地">{selectedOrg.location}</Descriptions.Item>
<Descriptions.Item label="所在地" span={isMobile ? 1 : 2}>
{selectedOrg.location}
</Descriptions.Item>
)}
{selectedOrg.color && (
<Descriptions.Item label="代表颜色">
{selectedOrg.color}
</Descriptions.Item>
)}
{selectedOrg.motto && (
<Descriptions.Item label="宗旨" span={2}>{selectedOrg.motto}</Descriptions.Item>
<Descriptions.Item label="格言/口号" span={2}>
{selectedOrg.motto}
</Descriptions.Item>
)}
<Descriptions.Item label="目标/宗旨" span={2}>
<Descriptions.Item label="组织目的" span={2}>
{selectedOrg.purpose}
</Descriptions.Item>
</Descriptions>
@@ -445,6 +559,77 @@ export default function Organizations() {
</Form.Item>
</Form>
</Modal>
{/* 编辑组织模态框 */}
<Modal
title="编辑组织信息"
open={isEditOrgModalOpen}
onCancel={() => {
setIsEditOrgModalOpen(false);
editOrgForm.resetFields();
}}
footer={null}
centered={!isMobile}
width={isMobile ? '100%' : 500}
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
styles={isMobile ? { body: { maxHeight: 'calc(100vh - 110px)', overflowY: 'auto' } } : undefined}
>
<Form
form={editOrgForm}
layout="vertical"
onFinish={async (values) => {
if (!selectedOrg) return;
try {
await axios.put(`/api/organizations/${selectedOrg.id}`, values);
message.success('组织信息更新成功');
setIsEditOrgModalOpen(false);
loadOrganizations();
} catch (error) {
message.error('更新失败');
console.error(error);
}
}}
>
<Form.Item
name="power_level"
label="势力等级"
rules={[{ required: true, message: '请输入势力等级' }]}
tooltip="0-100的数值,表示组织的影响力"
>
<InputNumber min={0} max={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="location"
label="所在地"
>
<Input placeholder="组织的主要活动区域或总部位置" />
</Form.Item>
<Form.Item
name="motto"
label="格言/口号"
>
<Input placeholder="组织的宗旨、格言或口号" />
</Form.Item>
<Form.Item
name="color"
label="代表颜色"
>
<Input placeholder="如:深红色、金色、黑色等" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsEditOrgModalOpen(false)}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
}
+23 -3
View File
@@ -974,7 +974,6 @@ export default function ProjectWizardNew() {
styles={{
body: {
maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(80vh - 110px)',
overflowY: 'auto'
}
}}
>
@@ -1051,12 +1050,29 @@ export default function ProjectWizardNew() {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="主要成员" name="organization_members">
<Input placeholder="如:张三、李四、王五" />
<Form.Item label="势力等级" name="power_level">
<InputNumber min={0} max={100} placeholder="0-100" style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="所在地" name="location">
<Input placeholder="组织所在地" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="代表颜色" name="color">
<Input placeholder="如:深红色、金色" />
</Form.Item>
</Col>
</Row>
<Form.Item label="格言/口号" name="motto">
<Input placeholder="组织的格言或口号" />
</Form.Item>
<Form.Item
label="组织目的"
name="organization_purpose"
@@ -1064,6 +1080,10 @@ export default function ProjectWizardNew() {
>
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
</Form.Item>
<Form.Item label="主要成员" name="organization_members">
<Input placeholder="如:张三、李四、王五" />
</Form.Item>
</>
)}
+10
View File
@@ -172,6 +172,11 @@ export interface Character {
organization_members?: string;
traits?: string;
avatar_url?: string;
// 组织扩展字段(从Organization表关联)
power_level?: number;
location?: string;
motto?: string;
color?: string;
created_at: string;
updated_at: string;
}
@@ -190,6 +195,11 @@ export interface CharacterUpdate {
organization_purpose?: string;
organization_members?: string;
traits?: string;
// 组织扩展字段
power_level?: number;
location?: string;
motto?: string;
color?: string;
}
// 章节类型定义