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