feature:新增提示词工坊功能
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user