style:1.组织管理页面支持组织列表滚动 2.优化一些页面的标题和图标显示
This commit is contained in:
+12
-14
@@ -8,7 +8,7 @@
|
||||
# 应用配置
|
||||
# ==========================================
|
||||
APP_NAME=MuMuAINovel
|
||||
APP_VERSION=1.1.4
|
||||
APP_VERSION=1.2.2
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
DEBUG=false
|
||||
@@ -25,7 +25,7 @@ POSTGRES_PASSWORD=123456
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# 数据库连接 URL(Docker 部署时自动生成)
|
||||
DATABASE_URL=postgresql+asyncpg://mumuai:123456@localhost:5432/mumuai_novel
|
||||
# DATABASE_URL=postgresql+asyncpg://mumuai:123456@localhost:5432/mumuai_novel
|
||||
|
||||
# ==========================================
|
||||
# SQLite 数据库配置
|
||||
@@ -33,13 +33,6 @@ DATABASE_URL=postgresql+asyncpg://mumuai:123456@localhost:5432/mumuai_novel
|
||||
|
||||
# DATABASE_URL=sqlite+aiosqlite:///data/ai_story.db
|
||||
|
||||
# ==========================================
|
||||
# 代理配置(可选)
|
||||
# ==========================================
|
||||
# HTTP_PROXY=http://your-proxy:port
|
||||
# HTTPS_PROXY=http://your-proxy:port
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# ==========================================
|
||||
# 日志配置
|
||||
# ==========================================
|
||||
@@ -54,6 +47,13 @@ LOG_BACKUP_COUNT=30
|
||||
# ==========================================
|
||||
CORS_ORIGINS=["http://localhost:8000","http://127.0.0.1:8000"]
|
||||
|
||||
# ==========================================
|
||||
# 代理配置(可选)
|
||||
# ==========================================
|
||||
# HTTP_PROXY=http://your-proxy:port
|
||||
# HTTPS_PROXY=http://your-proxy:port
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# ==========================================
|
||||
# AI 服务配置(至少配置一个)
|
||||
# ==========================================
|
||||
@@ -71,11 +71,9 @@ DEFAULT_MAX_TOKENS=32000
|
||||
# ==========================================
|
||||
# LinuxDO OAuth 配置(可选)
|
||||
# ==========================================
|
||||
# LINUXDO_CLIENT_ID=your_client_id_here
|
||||
# LINUXDO_CLIENT_SECRET=your_client_secret_here
|
||||
# LINUXDO_REDIRECT_URI=http://localhost:8000/api/auth/callback
|
||||
|
||||
# 前端 URL(OAuth 回调后重定向)
|
||||
LINUXDO_CLIENT_ID=11111
|
||||
LINUXDO_CLIENT_SECRET=11111
|
||||
LINUXDO_REDIRECT_URI=http://localhost:8000/api/auth/callback
|
||||
FRONTEND_URL=http://localhost:8000
|
||||
|
||||
# 初始管理员(LinuxDO user_id)
|
||||
|
||||
@@ -497,7 +497,7 @@ async def generate_organization_stream(
|
||||
- 其他要求:{gen_request.requirements or '无'}
|
||||
"""
|
||||
|
||||
yield await SSEResponse.send_progress("构建AI提示词...", 20)
|
||||
yield await SSEResponse.send_progress("构建AI提示词...", 5)
|
||||
|
||||
# 获取自定义提示词模板
|
||||
template = await PromptService.get_template("SINGLE_ORGANIZATION_GENERATION", user_id, db)
|
||||
@@ -508,7 +508,7 @@ async def generate_organization_stream(
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
yield await SSEResponse.send_progress("调用AI服务生成组织...", 30)
|
||||
yield await SSEResponse.send_progress("调用AI服务生成组织...", 10)
|
||||
logger.info(f"🎯 开始为项目 {gen_request.project_id} 生成组织(SSE流式)")
|
||||
|
||||
try:
|
||||
@@ -525,7 +525,7 @@ async def generate_organization_stream(
|
||||
|
||||
# 定期更新字数(5-95%,AI生成占90%)
|
||||
if chunk_count % 5 == 0:
|
||||
progress = min(5 + (chunk_count // 5), 95)
|
||||
progress = min(10 + (chunk_count // 5), 95)
|
||||
yield await SSEResponse.send_progress(
|
||||
f"AI生成组织中... ({len(ai_content)}字符)",
|
||||
progress
|
||||
@@ -544,7 +544,7 @@ async def generate_organization_stream(
|
||||
yield await SSEResponse.send_error("AI服务返回空响应")
|
||||
return
|
||||
|
||||
yield await SSEResponse.send_progress("解析AI响应...", 96)
|
||||
yield await SSEResponse.send_progress("解析AI响应...", 90)
|
||||
|
||||
# ✅ 使用统一的 JSON 清洗方法
|
||||
try:
|
||||
@@ -557,7 +557,7 @@ async def generate_organization_stream(
|
||||
yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}")
|
||||
return
|
||||
|
||||
yield await SSEResponse.send_progress("创建组织记录...", 97)
|
||||
yield await SSEResponse.send_progress("创建组织记录...", 95)
|
||||
|
||||
# 创建角色记录(组织也是角色的一种)
|
||||
character = Character(
|
||||
|
||||
@@ -134,10 +134,10 @@ async def world_building_generator(
|
||||
|
||||
请结合上述资料,生成符合历史/现实的世界观设定。"""
|
||||
final_prompt = enhanced_prompt
|
||||
yield await SSEResponse.send_progress("💡 已整合参考资料,开始生成世界观...", 30)
|
||||
yield await SSEResponse.send_progress("💡 已整合参考资料,开始生成世界观...", 10)
|
||||
else:
|
||||
final_prompt = base_prompt
|
||||
yield await SSEResponse.send_progress("正在调用AI生成...", 30)
|
||||
yield await SSEResponse.send_progress("正在调用AI生成...", 10)
|
||||
|
||||
# ===== 流式生成世界观(带重试机制) =====
|
||||
MAX_WORLD_RETRIES = 3 # 最多重试3次
|
||||
@@ -148,7 +148,7 @@ async def world_building_generator(
|
||||
while world_retry_count < MAX_WORLD_RETRIES and not world_generation_success:
|
||||
try:
|
||||
retry_suffix = f" (重试{world_retry_count}/{MAX_WORLD_RETRIES})" if world_retry_count > 0 else ""
|
||||
yield await SSEResponse.send_progress(f"生成世界观{retry_suffix}...", 30 + world_retry_count * 5)
|
||||
yield await SSEResponse.send_progress(f"生成世界观{retry_suffix}...", 10 + world_retry_count * 5)
|
||||
|
||||
# 流式生成世界观
|
||||
accumulated_text = ""
|
||||
@@ -181,7 +181,7 @@ async def world_building_generator(
|
||||
if world_retry_count < MAX_WORLD_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ AI返回为空,准备重试...",
|
||||
30 + world_retry_count * 5,
|
||||
10 + world_retry_count * 5,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
@@ -221,7 +221,7 @@ async def world_building_generator(
|
||||
if world_retry_count < MAX_WORLD_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ JSON解析失败,准备重试...",
|
||||
30 + world_retry_count * 5,
|
||||
10 + world_retry_count * 5,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
@@ -241,7 +241,7 @@ async def world_building_generator(
|
||||
if world_retry_count < MAX_WORLD_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ 生成异常,准备重试...",
|
||||
30 + world_retry_count * 5,
|
||||
10 + world_retry_count * 5,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
@@ -1599,10 +1599,10 @@ async def world_building_regenerate_generator(
|
||||
|
||||
请结合上述资料,生成符合历史/现实的世界观设定。"""
|
||||
final_prompt = enhanced_prompt
|
||||
yield await SSEResponse.send_progress("💡 已整合参考资料,开始生成世界观...", 30)
|
||||
yield await SSEResponse.send_progress("💡 已整合参考资料,开始生成世界观...", 10)
|
||||
else:
|
||||
final_prompt = base_prompt
|
||||
yield await SSEResponse.send_progress("正在调用AI生成...", 30)
|
||||
yield await SSEResponse.send_progress("正在调用AI生成...", 10)
|
||||
|
||||
# ===== 流式生成世界观(带重试机制) =====
|
||||
MAX_WORLD_RETRIES = 3 # 最多重试3次
|
||||
@@ -1613,7 +1613,7 @@ async def world_building_regenerate_generator(
|
||||
while world_retry_count < MAX_WORLD_RETRIES and not world_generation_success:
|
||||
try:
|
||||
retry_suffix = f" (重试{world_retry_count}/{MAX_WORLD_RETRIES})" if world_retry_count > 0 else ""
|
||||
yield await SSEResponse.send_progress(f"重新生成世界观{retry_suffix}...", 30 + world_retry_count * 5)
|
||||
yield await SSEResponse.send_progress(f"重新生成世界观{retry_suffix}...", 10 + world_retry_count * 5)
|
||||
|
||||
# 流式生成世界观
|
||||
accumulated_text = ""
|
||||
@@ -1630,7 +1630,7 @@ async def world_building_regenerate_generator(
|
||||
yield await SSEResponse.send_chunk(chunk)
|
||||
|
||||
if chunk_count % 5 == 0:
|
||||
progress = min(30 + (chunk_count // 5), 85)
|
||||
progress = min(10 + (chunk_count // 5), 85)
|
||||
yield await SSEResponse.send_progress(f"生成中... ({len(accumulated_text)}字符)", progress)
|
||||
|
||||
if chunk_count % 20 == 0:
|
||||
@@ -1643,7 +1643,7 @@ async def world_building_regenerate_generator(
|
||||
if world_retry_count < MAX_WORLD_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ AI返回为空,准备重试...",
|
||||
30 + world_retry_count * 5,
|
||||
10 + world_retry_count * 5,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
@@ -1679,7 +1679,7 @@ async def world_building_regenerate_generator(
|
||||
if world_retry_count < MAX_WORLD_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ JSON解析失败,准备重试...",
|
||||
30 + world_retry_count * 5,
|
||||
10 + world_retry_count * 5,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
@@ -1699,7 +1699,7 @@ async def world_building_regenerate_generator(
|
||||
if world_retry_count < MAX_WORLD_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ 生成异常,准备重试...",
|
||||
30 + world_retry_count * 5,
|
||||
10 + world_retry_count * 5,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# 获取版本信息(从config.py中提取)
|
||||
APP_VERSION=$(grep -oP "app_version:\s*str\s*=\s*\"\K[^\"]*" /app/app/config.py || echo "unknown")
|
||||
BUILD_TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "================================================"
|
||||
echo "🚀 MuMuAINovel 启动中..."
|
||||
echo "📦 版本: v${APP_VERSION}"
|
||||
echo "🕐 启动时间: ${BUILD_TIME}"
|
||||
echo "================================================"
|
||||
|
||||
# 数据库配置(从环境变量读取)
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ services:
|
||||
- DEFAULT_AI_PROVIDER=${DEFAULT_AI_PROVIDER:-openai}
|
||||
- DEFAULT_MODEL=${DEFAULT_MODEL:-gpt-4o-mini}
|
||||
- DEFAULT_TEMPERATURE=${DEFAULT_TEMPERATURE:-0.7}
|
||||
- DEFAULT_MAX_TOKENS=${DEFAULT_MAX_TOKENS:-2000}
|
||||
- DEFAULT_MAX_TOKENS=${DEFAULT_MAX_TOKENS:-32000}
|
||||
|
||||
# LinuxDO OAuth 配置
|
||||
- LINUXDO_CLIENT_ID=${LINUXDO_CLIENT_ID:-11111}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 275 KiB |
@@ -3,3 +3,17 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 移动端按钮文本优化 */
|
||||
@media (max-width: 576px) {
|
||||
.button-text-mobile {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 小屏幕下按钮文字可以隐藏,只保留图标 */
|
||||
@media (max-width: 480px) {
|
||||
.button-text-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GithubOutlined,
|
||||
ReloadOutlined,
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
fetchChangelog,
|
||||
@@ -29,6 +30,7 @@ interface ChangelogModalProps {
|
||||
// 提交类型图标和颜色配置
|
||||
const typeConfig: Record<ChangelogEntry['type'], { icon: React.ReactNode; color: string; label: string }> = {
|
||||
feature: { icon: <StarOutlined />, color: 'green', label: '新功能' },
|
||||
update: { icon: <SyncOutlined />, color: 'geekblue', label: '更新' },
|
||||
fix: { icon: <BugOutlined />, color: 'red', label: '修复' },
|
||||
docs: { icon: <FileTextOutlined />, color: 'blue', label: '文档' },
|
||||
style: { icon: <BgColorsOutlined />, color: 'purple', label: '样式' },
|
||||
|
||||
@@ -283,7 +283,10 @@ export default function Careers() {
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<Title level={3} style={{ margin: 0 }}>职业管理</Title>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
<TrophyOutlined style={{ marginRight: 8 }} />
|
||||
职业管理
|
||||
</Title>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="dashed"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
UnorderedListOutlined,
|
||||
FundOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import api from '../services/api';
|
||||
@@ -189,14 +190,30 @@ const ChapterAnalysis: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
gap: isMobile ? 0 : 16,
|
||||
flexDirection: isMobile ? 'column' : 'row'
|
||||
}}>
|
||||
{/* 左侧章节列表 - 桌面端 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
{/* 页面标题 - 仅桌面端显示 */}
|
||||
{!isMobile && (
|
||||
<div style={{
|
||||
padding: '16px 0',
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: 24 }}>
|
||||
<FundOutlined style={{ marginRight: 8 }} />
|
||||
剧情分析
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
gap: isMobile ? 0 : 16,
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 左侧章节列表 - 桌面端 */}
|
||||
{!isMobile && (
|
||||
<Card
|
||||
title="章节列表"
|
||||
style={{ width: 280, height: '100%', overflow: 'hidden' }}
|
||||
@@ -237,9 +254,9 @@ const ChapterAnalysis: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 移动端章节列表抽屉 */}
|
||||
{/* 移动端章节列表抽屉 */}
|
||||
{isMobile && (
|
||||
<Drawer
|
||||
title="章节列表"
|
||||
@@ -284,10 +301,10 @@ const ChapterAnalysis: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 右侧内容区域 */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
{/* 右侧内容区域 */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
{!selectedChapter ? (
|
||||
<Card style={{ height: '100%' }}>
|
||||
<Empty description="请从左侧选择一个章节查看" style={{ marginTop: 100 }} />
|
||||
@@ -530,6 +547,7 @@ const ChapterAnalysis: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1491,7 +1491,10 @@ export default function Chapters() {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: isMobile ? 'stretch' : 'center'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>章节管理</h2>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
|
||||
<BookOutlined style={{ marginRight: 8 }} />
|
||||
章节管理
|
||||
</h2>
|
||||
<Space direction={isMobile ? 'vertical' : 'horizontal'} style={{ width: isMobile ? '100%' : 'auto' }}>
|
||||
{currentProject.outline_mode === 'one-to-many' && (
|
||||
<Button
|
||||
|
||||
@@ -382,7 +382,10 @@ export default function Characters() {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: isMobile ? 'stretch' : 'center'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>角色与组织管理</h2>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
|
||||
<TeamOutlined style={{ marginRight: 8 }} />
|
||||
角色与组织管理
|
||||
</h2>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions, Drawer } from 'antd';
|
||||
import { PlusOutlined, UserOutlined, EditOutlined, DeleteOutlined, UnorderedListOutlined, BankOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync } from '../store/hooks';
|
||||
import axios from 'axios';
|
||||
@@ -57,6 +57,7 @@ export default function Organizations() {
|
||||
const [editOrgForm] = Form.useForm();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [orgListVisible, setOrgListVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -82,7 +83,7 @@ export default function Organizations() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, selectedOrg]);
|
||||
}, [projectId]);
|
||||
|
||||
const loadCharacters = useCallback(async () => {
|
||||
try {
|
||||
@@ -300,61 +301,169 @@ export default function Organizations() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
{contextHolder}
|
||||
<Card
|
||||
title={
|
||||
<Space wrap>
|
||||
<TeamOutlined />
|
||||
<span style={{ fontSize: isMobile ? 14 : 16 }}>组织管理</span>
|
||||
{!isMobile && <Tag color="blue">{currentProject?.title}</Tag>}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
|
||||
{/* 页面标题 - 仅桌面端显示 */}
|
||||
{!isMobile && (
|
||||
<div style={{
|
||||
display: isMobile ? 'flex' : 'grid',
|
||||
flexDirection: isMobile ? 'column' : undefined,
|
||||
gridTemplateColumns: isMobile ? undefined : '300px 1fr',
|
||||
gap: isMobile ? '16px' : '24px',
|
||||
maxHeight: isMobile ? 'calc(100vh - 200px)' : undefined,
|
||||
overflowY: isMobile ? 'auto' : undefined
|
||||
padding: '16px 0',
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
{/* 左侧:组织列表 */}
|
||||
<div>
|
||||
<Card
|
||||
size="small"
|
||||
title={`组织列表 (${organizations.length})`}
|
||||
loading={loading}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{organizations.map(org => (
|
||||
<Card
|
||||
key={org.id}
|
||||
size="small"
|
||||
hoverable
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: selectedOrg?.id === org.id ? '2px solid var(--color-primary)' : '1px solid var(--color-border-secondary)'
|
||||
}}
|
||||
onClick={() => handleSelectOrganization(org)}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<strong>{org.name}</strong>
|
||||
<Tag>{org.type}</Tag>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
成员: {org.member_count} | 势力: {org.power_level}
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
<h2 style={{ margin: 0, fontSize: 24 }}>
|
||||
<BankOutlined style={{ marginRight: 8 }} />
|
||||
组织管理
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
gap: isMobile ? 0 : 16,
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 左侧组织列表 - 桌面端 */}
|
||||
{!isMobile && (
|
||||
<Card
|
||||
title={`组织列表 (${organizations.length})`}
|
||||
style={{ width: 300, height: '100%', overflow: 'hidden' }}
|
||||
bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }}
|
||||
loading={loading}
|
||||
>
|
||||
{organizations.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}>
|
||||
暂无组织
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%', padding: '12px' }}>
|
||||
{organizations.map(org => (
|
||||
<Card
|
||||
key={org.id}
|
||||
size="small"
|
||||
hoverable
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent'
|
||||
}}
|
||||
onClick={() => handleSelectOrganization(org)}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
||||
<Tag color="blue">{org.type}</Tag>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
成员: {org.member_count} | 势力: {org.power_level}
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 右侧:组织详情和成员 */}
|
||||
<div style={{ minHeight: isMobile ? 'auto' : undefined }}>
|
||||
{selectedOrg ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
{/* 移动端组织列表抽屉 */}
|
||||
{isMobile && (
|
||||
<Drawer
|
||||
title="组织列表"
|
||||
placement="left"
|
||||
onClose={() => setOrgListVisible(false)}
|
||||
open={orgListVisible}
|
||||
width="85%"
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{organizations.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}>
|
||||
暂无组织
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%', padding: '12px' }}>
|
||||
{organizations.map(org => (
|
||||
<Card
|
||||
key={org.id}
|
||||
size="small"
|
||||
hoverable
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleSelectOrganization(org);
|
||||
setOrgListVisible(false);
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
||||
<Tag color="blue">{org.type}</Tag>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
成员: {org.member_count} | 势力: {org.power_level}
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
{/* 右侧内容区域 */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
{!selectedOrg ? (
|
||||
<Card style={{ height: '100%' }}>
|
||||
<div style={{ textAlign: 'center', padding: '100px 20px', color: '#999' }}>
|
||||
{isMobile && organizations.length > 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UnorderedListOutlined />}
|
||||
onClick={() => setOrgListVisible(true)}
|
||||
style={{ marginBottom: 20 }}
|
||||
>
|
||||
选择组织
|
||||
</Button>
|
||||
)}
|
||||
<div>请选择一个组织查看详情</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{/* 工具栏 - 移动端显示项目标题和组织列表按钮 */}
|
||||
{isMobile && (
|
||||
<Card size="small" style={{ marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<BankOutlined />
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
组织管理
|
||||
</span>
|
||||
<Tag color="blue">{currentProject?.title}</Tag>
|
||||
</Space>
|
||||
<Button
|
||||
icon={<UnorderedListOutlined />}
|
||||
onClick={() => setOrgListVisible(true)}
|
||||
size="small"
|
||||
>
|
||||
列表
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
gap: isMobile ? 0 : 16,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<Card
|
||||
style={{ flex: 1, overflow: 'auto' }}
|
||||
bodyStyle={{ padding: isMobile ? '12px' : '24px' }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={isMobile ? 'middle' : 'large'}>
|
||||
<Card
|
||||
title="组织详情"
|
||||
size="small"
|
||||
@@ -445,17 +554,13 @@ export default function Organizations() {
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
) : (
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
请从左侧选择一个组织查看详情
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 添加成员模态框 */}
|
||||
<Modal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tooltip, Tabs } from 'antd';
|
||||
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useOutlineSync } from '../store/hooks';
|
||||
import { cardStyles } from '../components/CardStyles';
|
||||
@@ -1904,7 +1904,10 @@ export default function Outline() {
|
||||
alignItems: isMobile ? 'stretch' : 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>故事大纲</h2>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
|
||||
<FileTextOutlined style={{ marginRight: 8 }} />
|
||||
故事大纲
|
||||
</h2>
|
||||
{currentProject?.outline_mode && (
|
||||
<Tag color={currentProject.outline_mode === 'one-to-one' ? 'blue' : 'green'} style={{ width: 'fit-content' }}>
|
||||
{currentProject.outline_mode === 'one-to-one' ? '传统模式 (1→1)' : '细化模式 (1→N)'}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, Slider, Input, Tabs, AutoComplete } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined, ApartmentOutlined, UserOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -308,7 +308,7 @@ export default function Relationships() {
|
||||
<Card
|
||||
title={
|
||||
<Space wrap>
|
||||
<TeamOutlined />
|
||||
<ApartmentOutlined />
|
||||
<span style={{ fontSize: isMobile ? 14 : 16 }}>关系管理</span>
|
||||
{!isMobile && <Tag color="blue">{currentProject?.title}</Tag>}
|
||||
</Space>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Space } from 'antd';
|
||||
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Flex } from 'antd';
|
||||
import { GlobalOutlined, EditOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
import { useStore } from '../store';
|
||||
@@ -174,39 +174,51 @@ export default function WorldSetting() {
|
||||
backgroundColor: '#fff',
|
||||
padding: '16px 0',
|
||||
marginBottom: 24,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
|
||||
<h2 style={{ margin: 0 }}>世界设定</h2>
|
||||
</div>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleRegenerate}
|
||||
disabled={isRegenerating}
|
||||
>
|
||||
AI重新生成
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
editForm.setFieldsValue({
|
||||
world_time_period: currentProject.world_time_period || '',
|
||||
world_location: currentProject.world_location || '',
|
||||
world_atmosphere: currentProject.world_atmosphere || '',
|
||||
world_rules: currentProject.world_rules || '',
|
||||
});
|
||||
setIsEditModalVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑世界观
|
||||
</Button>
|
||||
</Space>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
gap={12}
|
||||
wrap="wrap"
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', minWidth: 'fit-content' }}>
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
|
||||
<h2 style={{ margin: 0, whiteSpace: 'nowrap' }}>世界设定</h2>
|
||||
</div>
|
||||
<Flex gap={8} wrap="wrap" style={{ flex: '0 1 auto' }}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleRegenerate}
|
||||
disabled={isRegenerating}
|
||||
style={{
|
||||
minWidth: 'fit-content',
|
||||
flex: '1 1 auto'
|
||||
}}
|
||||
>
|
||||
<span className="button-text-mobile">AI重新生成</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
editForm.setFieldsValue({
|
||||
world_time_period: currentProject.world_time_period || '',
|
||||
world_location: currentProject.world_location || '',
|
||||
world_atmosphere: currentProject.world_atmosphere || '',
|
||||
world_rules: currentProject.world_rules || '',
|
||||
});
|
||||
setIsEditModalVisible(true);
|
||||
}}
|
||||
style={{
|
||||
minWidth: 'fit-content',
|
||||
flex: '1 1 auto'
|
||||
}}
|
||||
>
|
||||
<span className="button-text-mobile">编辑世界观</span>
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
|
||||
@@ -180,7 +180,10 @@ export default function WritingStyles() {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: isMobile ? 'stretch' : 'center'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>写作风格管理</h2>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
|
||||
<EditOutlined style={{ marginRight: 8 }} />
|
||||
写作风格管理
|
||||
</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface ChangelogEntry {
|
||||
};
|
||||
message: string;
|
||||
commitUrl: string;
|
||||
type: 'feature' | 'fix' | 'docs' | 'style' | 'refactor' | 'perf' | 'test' | 'chore' | 'other';
|
||||
type: 'feature' | 'fix' | 'docs' | 'style' | 'refactor' | 'perf' | 'test' | 'chore' | 'update' | 'other';
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
@@ -39,91 +39,111 @@ const GITHUB_API_BASE = 'https://api.github.com';
|
||||
const REPO_OWNER = 'xiamuceer-j';
|
||||
const REPO_NAME = 'MuMuAINovel';
|
||||
|
||||
/**
|
||||
* 提交类型映射表
|
||||
* 统一不同别名到标准类型
|
||||
*/
|
||||
const TYPE_MAPPING: Record<string, ChangelogEntry['type']> = {
|
||||
// 功能类
|
||||
'feat': 'feature',
|
||||
'feature': 'feature',
|
||||
'update': 'update',
|
||||
|
||||
// 修复类
|
||||
'fix': 'fix',
|
||||
|
||||
// 文档类
|
||||
'docs': 'docs',
|
||||
'doc': 'docs',
|
||||
|
||||
// 样式类
|
||||
'style': 'style',
|
||||
|
||||
// 重构类
|
||||
'refactor': 'refactor',
|
||||
|
||||
// 性能类
|
||||
'perf': 'perf',
|
||||
|
||||
// 测试类
|
||||
'test': 'test',
|
||||
|
||||
// 杂项类
|
||||
'chore': 'chore',
|
||||
};
|
||||
|
||||
/**
|
||||
* 从提交信息中解析类型和作用域
|
||||
* 支持常见的提交信息格式:
|
||||
* - type: message
|
||||
* - type(scope): message
|
||||
* - [type] message
|
||||
*
|
||||
* 匹配优先级(从高到低):
|
||||
* 1. 标准 Conventional Commits 格式: type(scope): message 或 type: message
|
||||
* 2. 方括号格式: [type] message
|
||||
* 3. 简单前缀格式: type: message(支持中文冒号)
|
||||
* 4. 关键词模糊匹配(中英文)
|
||||
*/
|
||||
function parseCommitType(message: string): { type: ChangelogEntry['type']; scope?: string; cleanMessage: string } {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
const lowerMessage = message.toLowerCase().trim();
|
||||
|
||||
// 第一优先级:精确匹配 update: 开头(在正则之前检查)
|
||||
if (lowerMessage.startsWith('update:')) {
|
||||
const cleanMsg = message.replace(/^update:\s*/i, '');
|
||||
return { type: 'feature', cleanMessage: cleanMsg };
|
||||
}
|
||||
|
||||
// 第二优先级:匹配标准 conventional commits 格式 type: message 或 type(scope): message
|
||||
const conventionalMatch = message.match(/^(feat|feature|fix|docs|style|refactor|perf|test|chore)(?:\(([^)]+)\))?\s*:\s*(.+)/i);
|
||||
// 优先级1:标准 Conventional Commits 格式 - type(scope): message 或 type: message
|
||||
// 匹配所有支持的类型
|
||||
const conventionalPattern = new RegExp(
|
||||
`^(${Object.keys(TYPE_MAPPING).join('|')})(?:\\(([^)]+)\\))?\\s*[:\\::]\\s*(.+)`,
|
||||
'i'
|
||||
);
|
||||
const conventionalMatch = message.match(conventionalPattern);
|
||||
if (conventionalMatch) {
|
||||
const typeStr = conventionalMatch[1].toLowerCase();
|
||||
const mappedType = typeStr === 'feature' ? 'feature' : typeStr as ChangelogEntry['type'];
|
||||
const mappedType = TYPE_MAPPING[typeStr] || 'other';
|
||||
return {
|
||||
type: mappedType,
|
||||
scope: conventionalMatch[2],
|
||||
cleanMessage: conventionalMatch[3],
|
||||
cleanMessage: conventionalMatch[3].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// 第三优先级:匹配 [type] message 格式
|
||||
const bracketMatch = message.match(/^\[(feat|feature|fix|docs|style|refactor|perf|test|chore|update)\]\s*(.+)/i);
|
||||
// 优先级2:方括号格式 - [type] message
|
||||
const bracketPattern = new RegExp(
|
||||
`^\\[(${Object.keys(TYPE_MAPPING).join('|')})\\]\\s*(.+)`,
|
||||
'i'
|
||||
);
|
||||
const bracketMatch = message.match(bracketPattern);
|
||||
if (bracketMatch) {
|
||||
const typeStr = bracketMatch[1].toLowerCase();
|
||||
const mappedType = (typeStr === 'update' || typeStr === 'feature') ? 'feature' : typeStr as ChangelogEntry['type'];
|
||||
const mappedType = TYPE_MAPPING[typeStr] || 'other';
|
||||
return {
|
||||
type: mappedType,
|
||||
cleanMessage: bracketMatch[2],
|
||||
cleanMessage: bracketMatch[2].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// 第四优先级:通过前缀精确匹配(避免误判)
|
||||
if (lowerMessage.startsWith('fix:')|| lowerMessage.startsWith('fix:')) {
|
||||
const cleanMsg = message.replace(/^fix:\s*/i, '');
|
||||
return { type: 'fix', cleanMessage: cleanMsg };
|
||||
}
|
||||
|
||||
if (lowerMessage.startsWith('perf:')) {
|
||||
const cleanMsg = message.replace(/^perf:\s*/i, '');
|
||||
return { type: 'perf', cleanMessage: cleanMsg };
|
||||
}
|
||||
|
||||
if (lowerMessage.startsWith('docs:')) {
|
||||
const cleanMsg = message.replace(/^docs:\s*/i, '');
|
||||
return { type: 'docs', cleanMessage: cleanMsg };
|
||||
}
|
||||
|
||||
if (lowerMessage.startsWith('feat:') || lowerMessage.startsWith('feature:')) {
|
||||
const cleanMsg = message.replace(/^(feat|feature):\s*/i, '');
|
||||
return { type: 'feature', cleanMessage: cleanMsg };
|
||||
}
|
||||
|
||||
// 第五优先级:关键词模糊匹配(仅当前面都不匹配时)
|
||||
if (lowerMessage.includes('修复') || lowerMessage.includes('fix')) {
|
||||
return { type: 'fix', cleanMessage: message };
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('优化') || lowerMessage.includes('perf')) {
|
||||
return { type: 'perf', cleanMessage: message };
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('文档') || lowerMessage.includes('doc')) {
|
||||
return { type: 'docs', cleanMessage: message };
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('新增') || lowerMessage.includes('添加') || lowerMessage.includes('增加')) {
|
||||
return { type: 'feature', cleanMessage: message };
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('样式') || lowerMessage.includes('style')) {
|
||||
return { type: 'style', cleanMessage: message };
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('重构') || lowerMessage.includes('refactor')) {
|
||||
return { type: 'refactor', cleanMessage: message };
|
||||
// 优先级3:简单前缀格式 - type: message(支持英文和中文冒号)
|
||||
for (const [key, value] of Object.entries(TYPE_MAPPING)) {
|
||||
const prefixPattern = new RegExp(`^${key}\\s*[:\\::]\\s*`, 'i');
|
||||
if (prefixPattern.test(lowerMessage)) {
|
||||
const cleanMsg = message.replace(prefixPattern, '').trim();
|
||||
return { type: value, cleanMessage: cleanMsg };
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级4:关键词模糊匹配(仅当前面都不匹配时)
|
||||
const keywordMap: Array<{ keywords: string[]; type: ChangelogEntry['type'] }> = [
|
||||
{ keywords: ['修复', 'fix'], type: 'fix' },
|
||||
{ keywords: ['优化', 'perf'], type: 'perf' },
|
||||
{ keywords: ['文档', 'document'], type: 'docs' },
|
||||
{ keywords: ['新增', '添加', '增加', 'add'], type: 'feature' },
|
||||
{ keywords: ['更新', 'update'], type: 'update' },
|
||||
{ keywords: ['样式', 'style'], type: 'style' },
|
||||
{ keywords: ['重构', 'refactor'], type: 'refactor' },
|
||||
{ keywords: ['测试', 'test'], type: 'test' },
|
||||
];
|
||||
|
||||
for (const { keywords, type } of keywordMap) {
|
||||
if (keywords.some(keyword => lowerMessage.includes(keyword))) {
|
||||
return { type, cleanMessage: message };
|
||||
}
|
||||
}
|
||||
|
||||
// 默认类型
|
||||
return { type: 'other', cleanMessage: message };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user