721 lines
23 KiB
TypeScript
721 lines
23 KiB
TypeScript
|
|
import { useState, useEffect, useCallback } from 'react';
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
Row,
|
||
|
|
Col,
|
||
|
|
Input,
|
||
|
|
Select,
|
||
|
|
Button,
|
||
|
|
Tag,
|
||
|
|
Space,
|
||
|
|
Empty,
|
||
|
|
Spin,
|
||
|
|
Modal,
|
||
|
|
Form,
|
||
|
|
message,
|
||
|
|
Tooltip,
|
||
|
|
Badge,
|
||
|
|
Tabs,
|
||
|
|
Typography,
|
||
|
|
Pagination,
|
||
|
|
Alert,
|
||
|
|
} from 'antd';
|
||
|
|
import {
|
||
|
|
SearchOutlined,
|
||
|
|
DownloadOutlined,
|
||
|
|
HeartOutlined,
|
||
|
|
HeartFilled,
|
||
|
|
CloudUploadOutlined,
|
||
|
|
EyeOutlined,
|
||
|
|
UserOutlined,
|
||
|
|
ClockCircleOutlined,
|
||
|
|
CheckCircleOutlined,
|
||
|
|
CloseCircleOutlined,
|
||
|
|
SyncOutlined,
|
||
|
|
DeleteOutlined,
|
||
|
|
CloudOutlined,
|
||
|
|
DisconnectOutlined,
|
||
|
|
} from '@ant-design/icons';
|
||
|
|
import { promptWorkshopApi } from '../services/api';
|
||
|
|
import type {
|
||
|
|
PromptWorkshopItem,
|
||
|
|
PromptSubmission,
|
||
|
|
PromptSubmissionCreate,
|
||
|
|
} from '../types';
|
||
|
|
import { PROMPT_CATEGORIES } from '../types';
|
||
|
|
|
||
|
|
const { TextArea } = Input;
|
||
|
|
const { Text, Paragraph } = Typography;
|
||
|
|
|
||
|
|
interface PromptWorkshopProps {
|
||
|
|
onImportSuccess?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps) {
|
||
|
|
const [items, setItems] = useState<PromptWorkshopItem[]>([]);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [total, setTotal] = useState(0);
|
||
|
|
const [currentPage, setCurrentPage] = useState(1);
|
||
|
|
const [pageSize] = useState(12);
|
||
|
|
|
||
|
|
// 筛选条件
|
||
|
|
const [category, setCategory] = useState<string>('');
|
||
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||
|
|
const [sortBy, setSortBy] = useState<'newest' | 'popular' | 'downloads'>('newest');
|
||
|
|
|
||
|
|
// 服务状态
|
||
|
|
const [serviceStatus, setServiceStatus] = useState<{
|
||
|
|
mode: string;
|
||
|
|
instance_id: string;
|
||
|
|
cloud_connected?: boolean;
|
||
|
|
} | null>(null);
|
||
|
|
|
||
|
|
// 提交相关
|
||
|
|
const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false);
|
||
|
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||
|
|
const [submitForm] = Form.useForm();
|
||
|
|
|
||
|
|
// 我的提交
|
||
|
|
const [mySubmissions, setMySubmissions] = useState<PromptSubmission[]>([]);
|
||
|
|
const [submissionsLoading, setSubmissionsLoading] = useState(false);
|
||
|
|
|
||
|
|
// 详情弹窗
|
||
|
|
const [detailItem, setDetailItem] = useState<PromptWorkshopItem | null>(null);
|
||
|
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||
|
|
|
||
|
|
// 导入状态
|
||
|
|
const [importingId, setImportingId] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const isMobile = window.innerWidth <= 768;
|
||
|
|
|
||
|
|
// 加载服务状态
|
||
|
|
useEffect(() => {
|
||
|
|
const checkStatus = async () => {
|
||
|
|
try {
|
||
|
|
const status = await promptWorkshopApi.getStatus();
|
||
|
|
setServiceStatus(status);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to check workshop status:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
checkStatus();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 加载工坊列表
|
||
|
|
const loadItems = useCallback(async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const response = await promptWorkshopApi.getItems({
|
||
|
|
category: category || undefined,
|
||
|
|
search: searchKeyword || undefined,
|
||
|
|
sort: sortBy,
|
||
|
|
page: currentPage,
|
||
|
|
limit: pageSize,
|
||
|
|
});
|
||
|
|
setItems(response.data?.items || []);
|
||
|
|
setTotal(response.data?.total || 0);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to load workshop items:', error);
|
||
|
|
message.error('加载提示词工坊失败');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}, [category, searchKeyword, sortBy, currentPage, pageSize]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadItems();
|
||
|
|
}, [loadItems]);
|
||
|
|
|
||
|
|
// 加载我的提交
|
||
|
|
const loadMySubmissions = async () => {
|
||
|
|
setSubmissionsLoading(true);
|
||
|
|
try {
|
||
|
|
const response = await promptWorkshopApi.getMySubmissions();
|
||
|
|
setMySubmissions(response.data?.items || []);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to load submissions:', error);
|
||
|
|
} finally {
|
||
|
|
setSubmissionsLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 导入到本地
|
||
|
|
const handleImport = async (item: PromptWorkshopItem) => {
|
||
|
|
setImportingId(item.id);
|
||
|
|
try {
|
||
|
|
await promptWorkshopApi.importItem(item.id);
|
||
|
|
message.success(`已导入「${item.name}」到本地写作风格`);
|
||
|
|
onImportSuccess?.();
|
||
|
|
// 刷新列表更新下载计数
|
||
|
|
loadItems();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to import item:', error);
|
||
|
|
message.error('导入失败');
|
||
|
|
} finally {
|
||
|
|
setImportingId(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 点赞
|
||
|
|
const handleLike = async (item: PromptWorkshopItem) => {
|
||
|
|
try {
|
||
|
|
const response = await promptWorkshopApi.toggleLike(item.id);
|
||
|
|
// 更新本地状态
|
||
|
|
setItems(prev => prev.map(i =>
|
||
|
|
i.id === item.id
|
||
|
|
? { ...i, is_liked: response.liked, like_count: response.like_count }
|
||
|
|
: i
|
||
|
|
));
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to toggle like:', error);
|
||
|
|
message.error('操作失败');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 提交新提示词
|
||
|
|
const handleSubmit = async (values: PromptSubmissionCreate) => {
|
||
|
|
setSubmitLoading(true);
|
||
|
|
try {
|
||
|
|
await promptWorkshopApi.submit({
|
||
|
|
...values,
|
||
|
|
tags: values.tags ? (values.tags as unknown as string).split(',').map((t: string) => t.trim()).filter(Boolean) : [],
|
||
|
|
});
|
||
|
|
message.success('提交成功,等待管理员审核');
|
||
|
|
setIsSubmitModalOpen(false);
|
||
|
|
submitForm.resetFields();
|
||
|
|
loadMySubmissions();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to submit:', error);
|
||
|
|
message.error('提交失败');
|
||
|
|
} finally {
|
||
|
|
setSubmitLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 撤回提交
|
||
|
|
const handleWithdraw = async (submissionId: string) => {
|
||
|
|
try {
|
||
|
|
await promptWorkshopApi.withdrawSubmission(submissionId);
|
||
|
|
message.success('已撤回');
|
||
|
|
loadMySubmissions();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to withdraw:', error);
|
||
|
|
message.error('撤回失败');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 查看详情
|
||
|
|
const handleViewDetail = async (item: PromptWorkshopItem) => {
|
||
|
|
try {
|
||
|
|
const response = await promptWorkshopApi.getItem(item.id);
|
||
|
|
setDetailItem(response.data);
|
||
|
|
setIsDetailModalOpen(true);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to load detail:', error);
|
||
|
|
message.error('加载详情失败');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 获取分类标签颜色
|
||
|
|
const getCategoryColor = (cat: string) => {
|
||
|
|
const colors: Record<string, string> = {
|
||
|
|
general: 'blue',
|
||
|
|
fantasy: 'purple',
|
||
|
|
martial: 'orange',
|
||
|
|
romance: 'pink',
|
||
|
|
scifi: 'cyan',
|
||
|
|
horror: 'red',
|
||
|
|
history: 'gold',
|
||
|
|
urban: 'green',
|
||
|
|
game: 'magenta',
|
||
|
|
other: 'default',
|
||
|
|
};
|
||
|
|
return colors[cat] || 'default';
|
||
|
|
};
|
||
|
|
|
||
|
|
// 获取分类名称
|
||
|
|
const getCategoryName = (cat: string) => {
|
||
|
|
return PROMPT_CATEGORIES[cat] || cat;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 获取分类选项列表
|
||
|
|
const categoryOptions = Object.entries(PROMPT_CATEGORIES).map(([value, label]) => ({
|
||
|
|
value,
|
||
|
|
label,
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 获取提交状态标签
|
||
|
|
const getStatusTag = (status: string) => {
|
||
|
|
const config: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
||
|
|
pending: { color: 'processing', icon: <ClockCircleOutlined />, text: '待审核' },
|
||
|
|
approved: { color: 'success', icon: <CheckCircleOutlined />, text: '已通过' },
|
||
|
|
rejected: { color: 'error', icon: <CloseCircleOutlined />, text: '已拒绝' },
|
||
|
|
};
|
||
|
|
const cfg = config[status] || config.pending;
|
||
|
|
return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 网格配置
|
||
|
|
const gridConfig = {
|
||
|
|
gutter: isMobile ? 8 : 16,
|
||
|
|
xs: 24,
|
||
|
|
sm: 12,
|
||
|
|
md: 8,
|
||
|
|
lg: 6,
|
||
|
|
xl: 6,
|
||
|
|
};
|
||
|
|
|
||
|
|
// 渲染工坊列表
|
||
|
|
const renderWorkshopList = () => (
|
||
|
|
<div>
|
||
|
|
{/* 服务状态 */}
|
||
|
|
{serviceStatus && !serviceStatus.cloud_connected && serviceStatus.mode === 'client' && (
|
||
|
|
<Alert
|
||
|
|
type="warning"
|
||
|
|
message="云端服务未连接"
|
||
|
|
description="无法访问提示词工坊,请检查网络连接或稍后重试"
|
||
|
|
icon={<DisconnectOutlined />}
|
||
|
|
showIcon
|
||
|
|
style={{ marginBottom: 16 }}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 筛选区域 */}
|
||
|
|
<div style={{
|
||
|
|
display: 'flex',
|
||
|
|
flexWrap: 'wrap',
|
||
|
|
gap: 12,
|
||
|
|
marginBottom: 16,
|
||
|
|
alignItems: 'center',
|
||
|
|
}}>
|
||
|
|
<Input
|
||
|
|
placeholder="搜索提示词..."
|
||
|
|
prefix={<SearchOutlined />}
|
||
|
|
value={searchKeyword}
|
||
|
|
onChange={e => setSearchKeyword(e.target.value)}
|
||
|
|
onPressEnter={() => { setCurrentPage(1); loadItems(); }}
|
||
|
|
style={{ width: isMobile ? '100%' : 200 }}
|
||
|
|
allowClear
|
||
|
|
/>
|
||
|
|
<Select
|
||
|
|
placeholder="选择分类"
|
||
|
|
value={category}
|
||
|
|
onChange={v => { setCategory(v); setCurrentPage(1); }}
|
||
|
|
style={{ width: isMobile ? '100%' : 150 }}
|
||
|
|
allowClear
|
||
|
|
>
|
||
|
|
{categoryOptions.map(cat => (
|
||
|
|
<Select.Option key={cat.value} value={cat.value}>{cat.label}</Select.Option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
<Select
|
||
|
|
value={sortBy}
|
||
|
|
onChange={v => { setSortBy(v); setCurrentPage(1); }}
|
||
|
|
style={{ width: isMobile ? '100%' : 120 }}
|
||
|
|
>
|
||
|
|
<Select.Option value="newest">最新发布</Select.Option>
|
||
|
|
<Select.Option value="popular">最受欢迎</Select.Option>
|
||
|
|
<Select.Option value="downloads">下载最多</Select.Option>
|
||
|
|
</Select>
|
||
|
|
<Button
|
||
|
|
icon={<SyncOutlined />}
|
||
|
|
onClick={() => { setCurrentPage(1); loadItems(); }}
|
||
|
|
>
|
||
|
|
刷新
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 列表区域 */}
|
||
|
|
<Spin spinning={loading}>
|
||
|
|
{items.length === 0 ? (
|
||
|
|
<Empty description="暂无提示词" />
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Row gutter={[gridConfig.gutter, gridConfig.gutter]}>
|
||
|
|
{items.map(item => (
|
||
|
|
<Col
|
||
|
|
key={item.id}
|
||
|
|
xs={gridConfig.xs}
|
||
|
|
sm={gridConfig.sm}
|
||
|
|
md={gridConfig.md}
|
||
|
|
lg={gridConfig.lg}
|
||
|
|
xl={gridConfig.xl}
|
||
|
|
>
|
||
|
|
<Card
|
||
|
|
hoverable
|
||
|
|
style={{ height: '100%', borderRadius: 12 }}
|
||
|
|
bodyStyle={{ padding: 16, display: 'flex', flexDirection: 'column', height: '100%' }}
|
||
|
|
actions={[
|
||
|
|
<Tooltip title="查看详情" key="view">
|
||
|
|
<EyeOutlined onClick={() => handleViewDetail(item)} />
|
||
|
|
</Tooltip>,
|
||
|
|
<Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like">
|
||
|
|
<span onClick={() => handleLike(item)}>
|
||
|
|
{item.is_liked ? (
|
||
|
|
<HeartFilled style={{ color: '#ff4d4f' }} />
|
||
|
|
) : (
|
||
|
|
<HeartOutlined />
|
||
|
|
)}
|
||
|
|
<span style={{ marginLeft: 4 }}>{item.like_count || 0}</span>
|
||
|
|
</span>
|
||
|
|
</Tooltip>,
|
||
|
|
<Tooltip title="导入到本地" key="import">
|
||
|
|
<Button
|
||
|
|
type="link"
|
||
|
|
size="small"
|
||
|
|
icon={<DownloadOutlined />}
|
||
|
|
loading={importingId === item.id}
|
||
|
|
onClick={() => handleImport(item)}
|
||
|
|
>
|
||
|
|
{item.download_count || 0}
|
||
|
|
</Button>
|
||
|
|
</Tooltip>,
|
||
|
|
]}
|
||
|
|
>
|
||
|
|
<div style={{ flex: 1 }}>
|
||
|
|
<Space style={{ marginBottom: 8 }} wrap>
|
||
|
|
<Text strong style={{ fontSize: 15 }}>{item.name}</Text>
|
||
|
|
<Tag color={getCategoryColor(item.category)}>
|
||
|
|
{getCategoryName(item.category)}
|
||
|
|
</Tag>
|
||
|
|
</Space>
|
||
|
|
|
||
|
|
{item.description && (
|
||
|
|
<Paragraph
|
||
|
|
type="secondary"
|
||
|
|
style={{ fontSize: 13, marginBottom: 8 }}
|
||
|
|
ellipsis={{ rows: 2 }}
|
||
|
|
>
|
||
|
|
{item.description}
|
||
|
|
</Paragraph>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Paragraph
|
||
|
|
style={{
|
||
|
|
fontSize: 12,
|
||
|
|
backgroundColor: '#fafafa',
|
||
|
|
padding: 8,
|
||
|
|
borderRadius: 4,
|
||
|
|
marginBottom: 8,
|
||
|
|
}}
|
||
|
|
ellipsis={{ rows: 3 }}
|
||
|
|
>
|
||
|
|
{item.prompt_content}
|
||
|
|
</Paragraph>
|
||
|
|
|
||
|
|
{item.tags && item.tags.length > 0 && (
|
||
|
|
<Space size={4} wrap>
|
||
|
|
{item.tags.slice(0, 3).map(tag => (
|
||
|
|
<Tag key={tag} style={{ fontSize: 11 }}>{tag}</Tag>
|
||
|
|
))}
|
||
|
|
{item.tags.length > 3 && (
|
||
|
|
<Tag style={{ fontSize: 11 }}>+{item.tags.length - 3}</Tag>
|
||
|
|
)}
|
||
|
|
</Space>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
||
|
|
<Space>
|
||
|
|
<span><UserOutlined /> {item.author_name || '匿名'}</span>
|
||
|
|
</Space>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
))}
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
{total > pageSize && (
|
||
|
|
<div style={{ marginTop: 24, textAlign: 'center' }}>
|
||
|
|
<Pagination
|
||
|
|
current={currentPage}
|
||
|
|
total={total}
|
||
|
|
pageSize={pageSize}
|
||
|
|
onChange={page => setCurrentPage(page)}
|
||
|
|
showSizeChanger={false}
|
||
|
|
showTotal={t => `共 ${t} 个提示词`}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</Spin>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
// 渲染我的提交
|
||
|
|
const renderMySubmissions = () => (
|
||
|
|
<div>
|
||
|
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
|
|
<Text>查看您提交的提示词及审核状态</Text>
|
||
|
|
<Button icon={<SyncOutlined />} onClick={loadMySubmissions}>
|
||
|
|
刷新
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Spin spinning={submissionsLoading}>
|
||
|
|
{mySubmissions.length === 0 ? (
|
||
|
|
<Empty description="暂无提交记录" />
|
||
|
|
) : (
|
||
|
|
<Row gutter={[16, 16]}>
|
||
|
|
{mySubmissions.map(sub => (
|
||
|
|
<Col key={sub.id} xs={24} sm={12} md={8} lg={6}>
|
||
|
|
<Card
|
||
|
|
style={{ borderRadius: 12 }}
|
||
|
|
bodyStyle={{ padding: 16 }}
|
||
|
|
>
|
||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
|
|
<Text strong>{sub.name}</Text>
|
||
|
|
{getStatusTag(sub.status)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Tag color={getCategoryColor(sub.category)}>
|
||
|
|
{getCategoryName(sub.category)}
|
||
|
|
</Tag>
|
||
|
|
|
||
|
|
<Paragraph
|
||
|
|
type="secondary"
|
||
|
|
style={{ fontSize: 12, marginBottom: 0 }}
|
||
|
|
ellipsis={{ rows: 2 }}
|
||
|
|
>
|
||
|
|
{sub.prompt_content}
|
||
|
|
</Paragraph>
|
||
|
|
|
||
|
|
{sub.status === 'rejected' && sub.review_note && (
|
||
|
|
<Alert
|
||
|
|
type="error"
|
||
|
|
message="拒绝原因"
|
||
|
|
description={sub.review_note}
|
||
|
|
style={{ fontSize: 12 }}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div style={{ fontSize: 12, color: '#999' }}>
|
||
|
|
提交时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{sub.status === 'pending' && (
|
||
|
|
<Button
|
||
|
|
type="link"
|
||
|
|
danger
|
||
|
|
size="small"
|
||
|
|
icon={<DeleteOutlined />}
|
||
|
|
onClick={() => handleWithdraw(sub.id)}
|
||
|
|
>
|
||
|
|
撤回
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</Space>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
))}
|
||
|
|
</Row>
|
||
|
|
)}
|
||
|
|
</Spin>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
{/* 标题和操作区 */}
|
||
|
|
<div style={{
|
||
|
|
display: 'flex',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
alignItems: 'center',
|
||
|
|
marginBottom: 16,
|
||
|
|
flexWrap: 'wrap',
|
||
|
|
gap: 12,
|
||
|
|
}}>
|
||
|
|
<Space>
|
||
|
|
<CloudOutlined style={{ fontSize: 20 }} />
|
||
|
|
<Text strong style={{ fontSize: 16 }}>提示词工坊</Text>
|
||
|
|
{serviceStatus?.mode === 'server' && (
|
||
|
|
<Badge status="success" text="服务端模式" />
|
||
|
|
)}
|
||
|
|
</Space>
|
||
|
|
<Button
|
||
|
|
type="primary"
|
||
|
|
icon={<CloudUploadOutlined />}
|
||
|
|
onClick={() => setIsSubmitModalOpen(true)}
|
||
|
|
>
|
||
|
|
分享我的提示词
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 标签页 */}
|
||
|
|
<Tabs
|
||
|
|
defaultActiveKey="browse"
|
||
|
|
onChange={key => key === 'submissions' && loadMySubmissions()}
|
||
|
|
items={[
|
||
|
|
{
|
||
|
|
key: 'browse',
|
||
|
|
label: '浏览工坊',
|
||
|
|
children: renderWorkshopList(),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'submissions',
|
||
|
|
label: (
|
||
|
|
<Badge count={mySubmissions.filter(s => s.status === 'pending').length} size="small">
|
||
|
|
我的提交
|
||
|
|
</Badge>
|
||
|
|
),
|
||
|
|
children: renderMySubmissions(),
|
||
|
|
},
|
||
|
|
]}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 提交弹窗 */}
|
||
|
|
<Modal
|
||
|
|
title="分享提示词到工坊"
|
||
|
|
open={isSubmitModalOpen}
|
||
|
|
onCancel={() => {
|
||
|
|
setIsSubmitModalOpen(false);
|
||
|
|
submitForm.resetFields();
|
||
|
|
}}
|
||
|
|
footer={null}
|
||
|
|
width={isMobile ? '100%' : 600}
|
||
|
|
>
|
||
|
|
<Alert
|
||
|
|
type="info"
|
||
|
|
message="提交须知"
|
||
|
|
description="您的提示词将提交给管理员审核,审核通过后会在工坊中展示。请确保内容原创且不含敏感信息。"
|
||
|
|
style={{ marginBottom: 16 }}
|
||
|
|
showIcon
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Form
|
||
|
|
form={submitForm}
|
||
|
|
layout="vertical"
|
||
|
|
onFinish={handleSubmit}
|
||
|
|
>
|
||
|
|
<Form.Item
|
||
|
|
name="name"
|
||
|
|
label="名称"
|
||
|
|
rules={[{ required: true, message: '请输入名称' }]}
|
||
|
|
>
|
||
|
|
<Input placeholder="给您的提示词起个名字" maxLength={50} />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item
|
||
|
|
name="category"
|
||
|
|
label="分类"
|
||
|
|
rules={[{ required: true, message: '请选择分类' }]}
|
||
|
|
>
|
||
|
|
<Select placeholder="选择分类">
|
||
|
|
{categoryOptions.map(cat => (
|
||
|
|
<Select.Option key={cat.value} value={cat.value}>{cat.label}</Select.Option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item name="description" label="描述">
|
||
|
|
<TextArea rows={2} placeholder="简要描述这个提示词的用途和效果" maxLength={200} />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item
|
||
|
|
name="prompt_content"
|
||
|
|
label="提示词内容"
|
||
|
|
rules={[{ required: true, message: '请输入提示词内容' }]}
|
||
|
|
>
|
||
|
|
<TextArea rows={6} placeholder="输入完整的提示词内容..." />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item name="tags" label="标签">
|
||
|
|
<Input placeholder="输入标签,多个用逗号分隔,如: 武侠,对话,细腻" />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item>
|
||
|
|
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||
|
|
<Button onClick={() => {
|
||
|
|
setIsSubmitModalOpen(false);
|
||
|
|
submitForm.resetFields();
|
||
|
|
}}>
|
||
|
|
取消
|
||
|
|
</Button>
|
||
|
|
<Button type="primary" htmlType="submit" loading={submitLoading}>
|
||
|
|
提交审核
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
</Form.Item>
|
||
|
|
</Form>
|
||
|
|
</Modal>
|
||
|
|
|
||
|
|
{/* 详情弹窗 */}
|
||
|
|
<Modal
|
||
|
|
title={detailItem?.name}
|
||
|
|
open={isDetailModalOpen}
|
||
|
|
onCancel={() => {
|
||
|
|
setIsDetailModalOpen(false);
|
||
|
|
setDetailItem(null);
|
||
|
|
}}
|
||
|
|
footer={[
|
||
|
|
<Button key="close" onClick={() => setIsDetailModalOpen(false)}>
|
||
|
|
关闭
|
||
|
|
</Button>,
|
||
|
|
<Button
|
||
|
|
key="import"
|
||
|
|
type="primary"
|
||
|
|
icon={<DownloadOutlined />}
|
||
|
|
loading={importingId === detailItem?.id}
|
||
|
|
onClick={() => detailItem && handleImport(detailItem)}
|
||
|
|
>
|
||
|
|
导入到本地
|
||
|
|
</Button>,
|
||
|
|
]}
|
||
|
|
width={isMobile ? '100%' : 700}
|
||
|
|
>
|
||
|
|
{detailItem && (
|
||
|
|
<div>
|
||
|
|
<Space style={{ marginBottom: 16 }} wrap>
|
||
|
|
<Tag color={getCategoryColor(detailItem.category)}>
|
||
|
|
{getCategoryName(detailItem.category)}
|
||
|
|
</Tag>
|
||
|
|
{detailItem.tags?.map(tag => (
|
||
|
|
<Tag key={tag}>{tag}</Tag>
|
||
|
|
))}
|
||
|
|
</Space>
|
||
|
|
|
||
|
|
{detailItem.description && (
|
||
|
|
<Paragraph style={{ marginBottom: 16 }}>
|
||
|
|
{detailItem.description}
|
||
|
|
</Paragraph>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div style={{
|
||
|
|
backgroundColor: '#f5f5f5',
|
||
|
|
padding: 16,
|
||
|
|
borderRadius: 8,
|
||
|
|
marginBottom: 16,
|
||
|
|
}}>
|
||
|
|
<Text strong style={{ display: 'block', marginBottom: 8 }}>提示词内容</Text>
|
||
|
|
<pre style={{
|
||
|
|
whiteSpace: 'pre-wrap',
|
||
|
|
wordBreak: 'break-word',
|
||
|
|
margin: 0,
|
||
|
|
fontSize: 13,
|
||
|
|
}}>
|
||
|
|
{detailItem.prompt_content}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={8}>
|
||
|
|
<Text type="secondary">作者</Text>
|
||
|
|
<div><UserOutlined /> {detailItem.author_name || '匿名'}</div>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Text type="secondary">点赞</Text>
|
||
|
|
<div><HeartOutlined /> {detailItem.like_count || 0}</div>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Text type="secondary">下载</Text>
|
||
|
|
<div><DownloadOutlined /> {detailItem.download_count || 0}</div>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|