fix:修复提示词工坊相关功能 5

This commit is contained in:
xiamuceer-j
2026-01-27 16:15:47 +08:00
parent 692dab6912
commit d5589dba6e
4 changed files with 229 additions and 194 deletions
+2
View File
@@ -16,6 +16,7 @@ import ChapterReader from './pages/ChapterReader';
import ChapterAnalysis from './pages/ChapterAnalysis'; import ChapterAnalysis from './pages/ChapterAnalysis';
import Foreshadows from './pages/Foreshadows'; import Foreshadows from './pages/Foreshadows';
import WritingStyles from './pages/WritingStyles'; import WritingStyles from './pages/WritingStyles';
import PromptWorkshop from './pages/PromptWorkshop';
import Settings from './pages/Settings'; import Settings from './pages/Settings';
import MCPPlugins from './pages/MCPPlugins'; import MCPPlugins from './pages/MCPPlugins';
import UserManagement from './pages/UserManagement'; import UserManagement from './pages/UserManagement';
@@ -62,6 +63,7 @@ function App() {
<Route path="chapter-analysis" element={<ChapterAnalysis />} /> <Route path="chapter-analysis" element={<ChapterAnalysis />} />
<Route path="foreshadows" element={<Foreshadows />} /> <Route path="foreshadows" element={<Foreshadows />} />
<Route path="writing-styles" element={<WritingStyles />} /> <Route path="writing-styles" element={<WritingStyles />} />
<Route path="prompt-workshop" element={<PromptWorkshop />} />
<Route path="sponsor" element={<Sponsor />} /> <Route path="sponsor" element={<Sponsor />} />
{/* <Route path="polish" element={<Polish />} /> */} {/* <Route path="polish" element={<Polish />} /> */}
</Route> </Route>
+7
View File
@@ -17,6 +17,7 @@ import {
HeartOutlined, HeartOutlined,
TrophyOutlined, TrophyOutlined,
BulbOutlined, BulbOutlined,
CloudOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks'; import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
@@ -151,6 +152,11 @@ export default function ProjectDetail() {
icon: <EditOutlined />, icon: <EditOutlined />,
label: <Link to={`/project/${projectId}/writing-styles`}></Link>, label: <Link to={`/project/${projectId}/writing-styles`}></Link>,
}, },
{
key: 'prompt-workshop',
icon: <CloudOutlined />,
label: <Link to={`/project/${projectId}/prompt-workshop`}></Link>,
},
// { // {
// key: 'polish', // key: 'polish',
// icon: <ToolOutlined />, // icon: <ToolOutlined />,
@@ -171,6 +177,7 @@ export default function ProjectDetail() {
if (path.includes('/foreshadows')) return 'foreshadows'; if (path.includes('/foreshadows')) return 'foreshadows';
if (path.includes('/chapters')) return 'chapters'; if (path.includes('/chapters')) return 'chapters';
if (path.includes('/writing-styles')) return 'writing-styles'; if (path.includes('/writing-styles')) return 'writing-styles';
if (path.includes('/prompt-workshop')) return 'prompt-workshop';
if (path.includes('/sponsor')) return 'sponsor'; if (path.includes('/sponsor')) return 'sponsor';
// if (path.includes('/polish')) return 'polish'; // if (path.includes('/polish')) return 'polish';
return 'sponsor'; // 默认选中赞助支持 return 'sponsor'; // 默认选中赞助支持
@@ -51,11 +51,7 @@ import { PROMPT_CATEGORIES } from '../types';
const { TextArea } = Input; const { TextArea } = Input;
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
interface PromptWorkshopProps { export default function PromptWorkshop() {
onImportSuccess?: () => void;
}
export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps) {
const [items, setItems] = useState<PromptWorkshopItem[]>([]); const [items, setItems] = useState<PromptWorkshopItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -120,11 +116,24 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
const [editForm] = Form.useForm(); const [editForm] = Form.useForm();
const [editLoading, setEditLoading] = useState(false); const [editLoading, setEditLoading] = useState(false);
// 当前活动的 Tab
const [activeTab, setActiveTab] = useState<string>('browse');
const isMobile = window.innerWidth <= 768; const isMobile = window.innerWidth <= 768;
// 判断是否为服务端管理员 // 判断是否为服务端管理员
const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin; const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin;
// 卡片网格配置 - 与 WritingStyles 保持一致
const gridConfig = {
gutter: isMobile ? 8 : 16,
xs: 24,
sm: 24,
md: 12,
lg: 8,
xl: 6,
};
// 加载服务状态和用户信息 // 加载服务状态和用户信息
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@@ -186,7 +195,6 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
try { try {
await promptWorkshopApi.importItem(item.id); await promptWorkshopApi.importItem(item.id);
message.success(`已导入「${item.name}」到本地写作风格`); message.success(`已导入「${item.name}」到本地写作风格`);
onImportSuccess?.();
// 刷新列表更新下载计数 // 刷新列表更新下载计数
loadItems(); loadItems();
} catch (error) { } catch (error) {
@@ -296,19 +304,9 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>; return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>;
}; };
// 网格配置 // 渲染筛选区域(固定在顶部)
const gridConfig = { const renderFilterBar = () => (
gutter: isMobile ? 8 : 16, <div style={{ marginBottom: 16 }}>
xs: 24,
sm: 12,
md: 8,
lg: 6,
xl: 6,
};
// 渲染工坊列表
const renderWorkshopList = () => (
<div>
{/* 服务状态 */} {/* 服务状态 */}
{serviceStatus && !serviceStatus.cloud_connected && serviceStatus.mode === 'client' && ( {serviceStatus && !serviceStatus.cloud_connected && serviceStatus.mode === 'client' && (
<Alert <Alert
@@ -322,11 +320,10 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
)} )}
{/* 筛选区域 */} {/* 筛选区域 */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 12, gap: 12,
marginBottom: 16,
alignItems: 'center', alignItems: 'center',
}}> }}>
<Input <Input
@@ -358,21 +355,27 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
<Select.Option value="popular"></Select.Option> <Select.Option value="popular"></Select.Option>
<Select.Option value="downloads"></Select.Option> <Select.Option value="downloads"></Select.Option>
</Select> </Select>
<Button <Button
icon={<SyncOutlined />} icon={<SyncOutlined />}
onClick={() => { setCurrentPage(1); loadItems(); }} onClick={() => { setCurrentPage(1); loadItems(); }}
> >
</Button> </Button>
</div> </div>
</div>
);
{/* 列表区域 */} // 渲染工坊列表(只有卡片部分,用于滚动区域)
<Spin spinning={loading}> const renderWorkshopList = () => (
{items.length === 0 ? ( <Spin spinning={loading}>
<Empty description="暂无提示词" /> {items.length === 0 ? (
) : ( <Empty description="暂无提示词" />
<> ) : (
<Row gutter={[gridConfig.gutter, gridConfig.gutter]}> <>
<Row
gutter={[0, gridConfig.gutter]}
style={{ marginLeft: 0, marginRight: 0 }}
>
{items.map(item => ( {items.map(item => (
<Col <Col
key={item.id} key={item.id}
@@ -381,11 +384,27 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
md={gridConfig.md} md={gridConfig.md}
lg={gridConfig.lg} lg={gridConfig.lg}
xl={gridConfig.xl} xl={gridConfig.xl}
style={{
paddingLeft: 0,
paddingRight: gridConfig.gutter / 2,
marginBottom: gridConfig.gutter
}}
> >
<Card <Card
hoverable hoverable
style={{ height: '100%', borderRadius: 12 }} style={{
bodyStyle={{ padding: 16, display: 'flex', flexDirection: 'column', height: '100%' }} height: '100%',
borderRadius: 12,
display: 'flex',
flexDirection: 'column',
border: '1px solid #f0f0f0',
}}
bodyStyle={{
padding: 16,
display: 'flex',
flexDirection: 'column',
flex: 1,
}}
actions={[ actions={[
<Tooltip title="查看详情" key="view"> <Tooltip title="查看详情" key="view">
<EyeOutlined onClick={() => handleViewDetail(item)} /> <EyeOutlined onClick={() => handleViewDetail(item)} />
@@ -413,9 +432,9 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
</Tooltip>, </Tooltip>,
]} ]}
> >
<div style={{ flex: 1 }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Space style={{ marginBottom: 8 }} wrap> <Space style={{ marginBottom: 12 }} wrap>
<Text strong style={{ fontSize: 15 }}>{item.name}</Text> <Text strong style={{ fontSize: 16 }}>{item.name}</Text>
<Tag color={getCategoryColor(item.category)}> <Tag color={getCategoryColor(item.category)}>
{getCategoryName(item.category)} {getCategoryName(item.category)}
</Tag> </Tag>
@@ -424,28 +443,31 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
{item.description && ( {item.description && (
<Paragraph <Paragraph
type="secondary" type="secondary"
style={{ fontSize: 13, marginBottom: 8 }} style={{ fontSize: 13, marginBottom: 12 }}
ellipsis={{ rows: 2 }} ellipsis={{ rows: 2, tooltip: item.description }}
> >
{item.description} {item.description}
</Paragraph> </Paragraph>
)} )}
<Paragraph <Paragraph
type="secondary"
style={{ style={{
fontSize: 12, fontSize: 12,
marginBottom: 0,
backgroundColor: '#fafafa', backgroundColor: '#fafafa',
padding: 8, padding: 8,
borderRadius: 4, borderRadius: 4,
marginBottom: 8, flex: 1,
minHeight: 60,
}} }}
ellipsis={{ rows: 3 }} ellipsis={{ rows: 3, tooltip: item.prompt_content }}
> >
{item.prompt_content} {item.prompt_content}
</Paragraph> </Paragraph>
{item.tags && item.tags.length > 0 && ( {item.tags && item.tags.length > 0 && (
<Space size={4} wrap> <Space size={4} wrap style={{ marginTop: 8 }}>
{item.tags.slice(0, 3).map(tag => ( {item.tags.slice(0, 3).map(tag => (
<Tag key={tag} style={{ fontSize: 11 }}>{tag}</Tag> <Tag key={tag} style={{ fontSize: 11 }}>{tag}</Tag>
))} ))}
@@ -464,24 +486,23 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
</Card> </Card>
</Col> </Col>
))} ))}
</Row> </Row>
{total > pageSize && ( {total > pageSize && (
<div style={{ marginTop: 24, textAlign: 'center' }}> <div style={{ marginTop: 24, textAlign: 'center', paddingBottom: 16 }}>
<Pagination <Pagination
current={currentPage} current={currentPage}
total={total} total={total}
pageSize={pageSize} pageSize={pageSize}
onChange={page => setCurrentPage(page)} onChange={page => setCurrentPage(page)}
showSizeChanger={false} showSizeChanger={false}
showTotal={t => `${t} 个提示词`} showTotal={t => `${t} 个提示词`}
/> />
</div> </div>
)} )}
</> </>
)} )}
</Spin> </Spin>
</div>
); );
// 渲染我的提交 // 渲染我的提交
@@ -495,14 +516,26 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
</div> </div>
<Spin spinning={submissionsLoading}> <Spin spinning={submissionsLoading}>
{mySubmissions.length === 0 ? ( {mySubmissions.length === 0 ? (
<Empty description="暂无提交记录" /> <Empty description="暂无提交记录" />
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[0, gridConfig.gutter]} style={{ marginLeft: 0, marginRight: 0 }}>
{mySubmissions.map(sub => ( {mySubmissions.map(sub => (
<Col key={sub.id} xs={24} sm={12} md={8} lg={6}> <Col
key={sub.id}
xs={gridConfig.xs}
sm={gridConfig.sm}
md={gridConfig.md}
lg={gridConfig.lg}
xl={gridConfig.xl}
style={{
paddingLeft: 0,
paddingRight: gridConfig.gutter / 2,
marginBottom: gridConfig.gutter
}}
>
<Card <Card
style={{ borderRadius: 12 }} style={{ borderRadius: 12, height: '100%', border: '1px solid #f0f0f0' }}
bodyStyle={{ padding: 16 }} bodyStyle={{ padding: 16 }}
> >
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
@@ -551,8 +584,8 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
</Card> </Card>
</Col> </Col>
))} ))}
</Row> </Row>
)} )}
</Spin> </Spin>
</div> </div>
); );
@@ -754,11 +787,23 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
{adminSubmissions.length === 0 ? ( {adminSubmissions.length === 0 ? (
<Empty description="暂无待审核提交" /> <Empty description="暂无待审核提交" />
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[0, gridConfig.gutter]} style={{ marginLeft: 0, marginRight: 0 }}>
{adminSubmissions.map(sub => ( {adminSubmissions.map(sub => (
<Col key={sub.id} xs={24} sm={12} md={8} lg={6}> <Col
key={sub.id}
xs={gridConfig.xs}
sm={gridConfig.sm}
md={gridConfig.md}
lg={gridConfig.lg}
xl={gridConfig.xl}
style={{
paddingLeft: 0,
paddingRight: gridConfig.gutter / 2,
marginBottom: gridConfig.gutter
}}
>
<Card <Card
style={{ borderRadius: 12 }} style={{ borderRadius: 12, border: '1px solid #f0f0f0' }}
bodyStyle={{ padding: 16 }} bodyStyle={{ padding: 16 }}
actions={[ actions={[
<Button <Button
@@ -817,11 +862,23 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
{publishedItems.length === 0 ? ( {publishedItems.length === 0 ? (
<Empty description="暂无已发布提示词" /> <Empty description="暂无已发布提示词" />
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[0, gridConfig.gutter]} style={{ marginLeft: 0, marginRight: 0 }}>
{publishedItems.map(item => ( {publishedItems.map(item => (
<Col key={item.id} xs={24} sm={12} md={8} lg={6}> <Col
key={item.id}
xs={gridConfig.xs}
sm={gridConfig.sm}
md={gridConfig.md}
lg={gridConfig.lg}
xl={gridConfig.xl}
style={{
paddingLeft: 0,
paddingRight: gridConfig.gutter / 2,
marginBottom: gridConfig.gutter
}}
>
<Card <Card
style={{ borderRadius: 12 }} style={{ borderRadius: 12, border: '1px solid #f0f0f0' }}
bodyStyle={{ padding: 16 }} bodyStyle={{ padding: 16 }}
actions={[ actions={[
<Tooltip title="编辑" key="edit"> <Tooltip title="编辑" key="edit">
@@ -875,68 +932,79 @@ export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps)
); );
return ( return (
<div> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* 标题和操作区 */} {/* 固定区域:标题 + Tabs切换栏 + 筛选栏 */}
<div style={{ <div style={{ flexShrink: 0 }}>
display: 'flex', {/* 标题和操作区 */}
justifyContent: 'space-between', <div style={{
alignItems: 'center', display: 'flex',
marginBottom: 16, justifyContent: 'space-between',
flexWrap: 'wrap', alignItems: 'center',
gap: 12, padding: isMobile ? '12px 0' : '16px 0',
}}> marginBottom: isMobile ? 12 : 16,
<Space> borderBottom: '1px solid #f0f0f0',
<CloudOutlined style={{ fontSize: 20 }} /> flexWrap: 'wrap',
<Text strong style={{ fontSize: 16 }}></Text> gap: 12,
{serviceStatus?.mode === 'server' && ( }}>
<Badge status="success" text="服务端模式" /> <h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24, display: 'flex', alignItems: 'center', gap: 8 }}>
)} <CloudOutlined />
</Space>
<Button {serviceStatus?.mode === 'server' && (
type="primary" <Badge status="success" text="服务端模式" style={{ marginLeft: 8, fontSize: 12 }} />
icon={<CloudUploadOutlined />} )}
onClick={() => setIsSubmitModalOpen(true)} </h2>
> <Button
type="primary"
</Button> icon={<CloudUploadOutlined />}
onClick={() => setIsSubmitModalOpen(true)}
>
</Button>
</div>
{/* Tabs 切换栏(不含内容) */}
<Tabs
activeKey={activeTab}
onChange={key => {
setActiveTab(key);
if (key === 'submissions') loadMySubmissions();
if (key === 'admin') {
loadAdminSubmissions();
loadPublishedItems();
}
}}
items={[
{ key: 'browse', label: '浏览工坊' },
{
key: 'submissions',
label: (
<Badge count={mySubmissions.filter(s => s.status === 'pending').length} size="small">
</Badge>
),
},
...(isServerAdmin ? [{
key: 'admin',
label: (
<Badge count={adminPendingCount} size="small">
<span><SettingOutlined /> </span>
</Badge>
),
}] : []),
]}
tabBarStyle={{ marginBottom: 16 }}
/>
{/* 筛选栏(仅在浏览工坊时显示) */}
{activeTab === 'browse' && renderFilterBar()}
</div> </div>
{/* 标签页 */} {/* 滚动区域:只有卡片列表滚动 */}
<Tabs <div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
defaultActiveKey="browse" {activeTab === 'browse' && renderWorkshopList()}
onChange={key => { {activeTab === 'submissions' && renderMySubmissions()}
if (key === 'submissions') loadMySubmissions(); {activeTab === 'admin' && renderAdminPanel()}
if (key === 'admin') { </div>
loadAdminSubmissions();
loadPublishedItems();
}
}}
items={[
{
key: 'browse',
label: '浏览工坊',
children: renderWorkshopList(),
},
{
key: 'submissions',
label: (
<Badge count={mySubmissions.filter(s => s.status === 'pending').length} size="small">
</Badge>
),
children: renderMySubmissions(),
},
...(isServerAdmin ? [{
key: 'admin',
label: (
<Badge count={adminPendingCount} size="small">
<span><SettingOutlined /> </span>
</Badge>
),
children: renderAdminPanel(),
}] : []),
]}
/>
{/* 提交弹窗 */} {/* 提交弹窗 */}
<Modal <Modal
+16 -58
View File
@@ -13,8 +13,6 @@ import {
Typography, Typography,
Row, Row,
Col, Col,
Tabs,
Badge,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@@ -22,12 +20,10 @@ import {
DeleteOutlined, DeleteOutlined,
StarOutlined, StarOutlined,
StarFilled, StarFilled,
CloudOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import { writingStyleApi } from '../services/api'; import { writingStyleApi } from '../services/api';
import type { WritingStyle, WritingStyleCreate, WritingStyleUpdate } from '../types'; import type { WritingStyle, WritingStyleCreate, WritingStyleUpdate } from '../types';
import PromptWorkshop from '../components/PromptWorkshop';
const { TextArea } = Input; const { TextArea } = Input;
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
@@ -168,14 +164,24 @@ export default function WritingStyles() {
return styleType === 'preset' ? '预设' : '自定义'; return styleType === 'preset' ? '预设' : '自定义';
}; };
// 渲染本地风格列表 return (
const renderLocalStyles = () => ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div>
<div style={{ <div style={{
marginBottom: 16, position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'space-between',
alignItems: 'center',
}}> }}>
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
<EditOutlined style={{ marginRight: 8 }} />
</h2>
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined />}
@@ -184,7 +190,7 @@ export default function WritingStyles() {
</Button> </Button>
</div> </div>
<div style={{ flex: 1, overflowY: 'auto' }}> <div style={{ flex: 1, overflowY: 'auto' }}>
{styles.length === 0 ? ( {styles.length === 0 ? (
<Empty description="暂无风格数据" /> <Empty description="暂无风格数据" />
@@ -303,54 +309,6 @@ export default function WritingStyles() {
</Row> </Row>
)} )}
</div> </div>
</div>
);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
}}>
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
<EditOutlined style={{ marginRight: 8 }} />
</h2>
</div>
<Tabs
defaultActiveKey="local"
style={{ flex: 1 }}
items={[
{
key: 'local',
label: (
<span>
<EditOutlined />
</span>
),
children: renderLocalStyles(),
},
{
key: 'workshop',
label: (
<Badge dot>
<span>
<CloudOutlined />
</span>
</Badge>
),
children: <PromptWorkshop onImportSuccess={loadStyles} />,
},
]}
/>
{/* 创建自定义风格 Modal */} {/* 创建自定义风格 Modal */}
<Modal <Modal