diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index 55ce6441..f77b7005 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -38,6 +38,8 @@ import ContactImport from './pages/contact-import/ContactImport'; import Content from './pages/content/Content'; import TrafficPoolDetail from './pages/traffic-pool/TrafficPoolDetail'; import NewContent from './pages/content/NewContent'; +import Materials from './pages/content/materials/List'; +import MaterialsNew from './pages/content/materials/New'; function App() { // 初始化HTTP拦截器 @@ -91,6 +93,10 @@ function App() { } /> } /> } /> + } /> + } /> + } /> + } /> {/* 你可以继续添加更多路由 */} diff --git a/nkebao/src/pages/content/Content.tsx b/nkebao/src/pages/content/Content.tsx index 211bb080..d64147f8 100644 --- a/nkebao/src/pages/content/Content.tsx +++ b/nkebao/src/pages/content/Content.tsx @@ -207,7 +207,7 @@ export default function Content() { }; const handleEdit = (id: string) => { - navigate(`/content/new/${id}`); + navigate(`/content/edit/${id}`); }; const handleDelete = async (id: string) => { @@ -226,7 +226,7 @@ export default function Content() { }; const handleViewMaterials = (id: string) => { - navigate(`/content/${id}/materials`); + navigate(`/content/materials/${id}`); }; const handleSearch = () => { diff --git a/nkebao/src/pages/content/NewContent.tsx b/nkebao/src/pages/content/NewContent.tsx index d5d8c9c8..e7d560c0 100644 --- a/nkebao/src/pages/content/NewContent.tsx +++ b/nkebao/src/pages/content/NewContent.tsx @@ -11,7 +11,7 @@ import { Collapse, CollapsePanel ,Button} from 'tdesign-mobile-react'; import { toast } from '@/components/ui/toast'; import FriendSelection from '@/components/FriendSelection'; import GroupSelection from '@/components/GroupSelection'; -import { post } from '@/api/request'; +import { get, post } from '@/api/request'; // TODO: 引入微信好友/群组选择器、日期选择器等组件 interface WechatFriend { id: string; nickname: string; avatar: string; } @@ -33,6 +33,8 @@ interface ContentLibraryForm { export default function NewContentLibraryPage() { const navigate = useNavigate(); + const { id } = useParams(); + const isEdit = !!id; const [form, setForm] = useState({ name: '', sourceType: 'friends', @@ -52,13 +54,51 @@ export default function NewContentLibraryPage() { const [isGroupSelectorOpen, setIsGroupSelectorOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); + useEffect(() => { + if (isEdit) { + (async () => { + const res = await get(`/v1/content/library/detail?id=${id}`); + if (res && res.code === 200 && res.data) { + const data = res.data; + // 时间戳转YYYY-MM-DD + const formatDate = (val: number) => { + if (!val || val === 0 || typeof val !== 'number' || isNaN(val) || val < 1000000000) return ''; + try { + const d = new Date(val * 1000); + if (isNaN(d.getTime())) return ''; + return d.toISOString().slice(0, 10); + } catch { + return ''; + } + }; + setForm(f => ({ + ...f, + name: data.name || '', + sourceType: data.sourceType === 1 ? 'friends' : 'groups', + keywordsInclude: (data.keywordInclude || []).join(','), + keywordsExclude: (data.keywordExclude || []).join(','), + startDate: formatDate(data.timeStart), + endDate: formatDate(data.timeEnd), + selectedFriends: (data.selectedFriends || data.sourceFriends || []).map((fid: number | string) => ({ id: String(fid), nickname: String(fid), avatar: '' })), + selectedGroups: (data.sourceGroups || []).map((gid: number | string) => ({ id: String(gid), name: String(gid), avatar: '' })), + useAI: data.aiEnabled === 1, + aiPrompt: data.aiPrompt || '', + enabled: data.status === 1, + })); + setSelectedFriendObjs((data.selectedFriends || data.sourceFriends || []).map((fid: number | string) => ({ id: String(fid), nickname: String(fid), avatar: '' }))); + setSelectedGroupObjs((data.sourceGroups || []).map((gid: number | string) => ({ id: String(gid), name: String(gid), avatar: '' }))); + } + })(); + } + }, [isEdit, id]); + // TODO: 选择器、日期选择器等逻辑 const handleSave = async () => { setIsSubmitting(true); try { - // 组装提交参数 const payload = { + id: isEdit ? id : undefined, name: form.name, sourceType: form.sourceType === 'friends' ? 1 : 2, friends: form.selectedFriends.map(f => Number(f.id)), @@ -72,11 +112,15 @@ export default function NewContentLibraryPage() { endTime: form.endDate || '', status: form.enabled ? 1 : 0 }; - await post('/v1/content/library/create', payload); - toast({ title: '创建成功', description: '内容库已保存' }); + if (isEdit) { + await post('/v1/content/library/update', payload); + } else { + await post('/v1/content/library/create', payload); + } + toast({ title: isEdit ? '保存成功' : '创建成功', description: '内容库已保存' }); navigate('/content'); } catch (error) { - toast({ title: '创建失败', description: '保存内容库失败', variant: 'destructive' }); + toast({ title: isEdit ? '保存失败' : '创建失败', description: '保存内容库失败', variant: 'destructive' }); } finally { setIsSubmitting(false); } @@ -84,11 +128,11 @@ export default function NewContentLibraryPage() { return ( navigate(-1)} />} + header={ navigate(-1)} />} footer={
} diff --git a/nkebao/src/pages/content/materials/List.tsx b/nkebao/src/pages/content/materials/List.tsx new file mode 100644 index 00000000..ad5ed5ef --- /dev/null +++ b/nkebao/src/pages/content/materials/List.tsx @@ -0,0 +1,192 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import Layout from '@/components/Layout'; +import UnifiedHeader from '@/components/UnifiedHeader'; +import { Input } from '@/components/ui/input'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/components/ui/toast'; +import { get, del } from '@/api/request'; +import { Plus, Search, Edit, Trash2, UserCircle2, Tag, BarChart } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; + +interface MaterialItem { + id: string; + content: string; + tags: string[]; + type?: string; // 可选: text/image/video/link + images?: string[]; + video?: string; + createTime?: string; + status?: string; + title?: string; // Added for new card structure + creatorName?: string; // Added for new card structure + aiAnalysis?: string; // Added for AI analysis result +} + +export default function Materials() { + const navigate = useNavigate(); + const { id } = useParams(); + const [materials, setMaterials] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [loading, setLoading] = useState(false); + const [aiDialogOpen, setAiDialogOpen] = useState(false); + const [selectedMaterial, setSelectedMaterial] = useState(null); + + // 拉取素材列表 + const fetchMaterials = async () => { + setLoading(true); + try { + const res = await get(`/v1/content/library/item-list?page=1&limit=100&libraryId=${id}${searchQuery ? `&keyword=${encodeURIComponent(searchQuery)}` : ''}`); + if (res && res.code === 200 && Array.isArray(res.data?.list)) { + setMaterials(res.data.list); + } else { + setMaterials([]); + toast({ title: '获取失败', description: res?.msg || '获取素材列表失败' }); + } + } catch (error: any) { + setMaterials([]); + toast({ title: '网络错误', description: error?.message || '请检查网络连接' }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMaterials(); + // eslint-disable-next-line + }, [id]); + + const handleSearch = () => { + fetchMaterials(); + }; + + const handleDelete = async (materialId: string) => { + if (!window.confirm('确定要删除该素材吗?')) return; + try { + const res = await del(`/v1/content/library/material/delete?id=${materialId}`); + if (res && res.code === 200) { + toast({ title: '删除成功', description: '素材已删除' }); + fetchMaterials(); + } else { + toast({ title: '删除失败', description: res?.msg || '删除素材失败' }); + } + } catch (error: any) { + toast({ title: '网络错误', description: error?.message || '请检查网络连接' }); + } + }; + + const handleNewMaterial = () => { + navigate(`/content/materials/new/${id}`); + }; + + const handleEdit = (materialId: string) => { + navigate(`/content/materials/edit/${id}/${materialId}`); + }; + + return ( + + navigate(-1)} + + rightContent={ + <> + + + }/> +
+
+ + setSearchQuery(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleSearch(); }} + className="pl-9" + /> +
+ + +
+ + + } + > +
+
+
+ {loading ? ( +
加载中...
+ ) : materials.length === 0 ? ( +
暂无素材
+ ) : ( + materials.map((item) => ( +
+ {/* 顶部头像+系统创建+ID */} +
+
+ +
+
+ 系统创建 + + ID: {item.id} + +
+
+ {/* 标题 */} +
{item.title ? `【${item.title}】` : (item.content.length > 20 ? `【${item.content.slice(0, 20)}...】` : `【${item.content}】`)}
+ {/* 内容 */} +
{item.content}
+ {/* 标签 */} + {item.tags && item.tags.length > 0 && ( +
+ {item.tags.map((tag, index) => ( + + + {tag} + + ))} +
+ )} + {/* 操作按钮区 */} +
+
+ + + + + + AI 分析结果 + +
+

{selectedMaterial?.aiAnalysis || '正在分析中...'}

+
+
+
+
+ +
+
+ )) + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/content/materials/New.tsx b/nkebao/src/pages/content/materials/New.tsx new file mode 100644 index 00000000..902d65c9 --- /dev/null +++ b/nkebao/src/pages/content/materials/New.tsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { toast } from '@/components/ui/toast'; +import { Plus, X } from 'lucide-react'; +import Layout from '@/components/Layout'; +import UnifiedHeader from '@/components/UnifiedHeader'; +import { get, post, put } from '@/api/request'; +import { Image as ImageIcon, Link as LinkIcon, Video as VideoIcon, FileText, Layers, UploadCloud } from 'lucide-react'; +import { Upload } from 'tdesign-mobile-react'; + +export default function NewMaterial() { + const navigate = useNavigate(); + const { id, materialId } = useParams(); // materialId 作为编辑标识 + const [content, setContent] = useState(''); + const [comment, setComment] = useState(''); + const [contentType, setContentType] = useState(1); + const [desc, setDesc] = useState(''); + const [coverImage, setCoverImage] = useState(''); + const [url, setUrl] = useState(''); + const [videoUrl, setVideoUrl] = useState(''); + const [resUrls, setResUrls] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isEdit, setIsEdit] = useState(false); + const [sendTime, setSendTime] = useState(''); + const [images, setImages] = useState([]); + const [selectOpen, setSelectOpen] = useState(false); + + // 判断模式并拉取详情 + useEffect(() => { + if (materialId) { + setIsEdit(true); + get(`/v1/content/library/get-item-detail?id=${materialId}`) + .then(res => { + if (res && res.code === 200 && res.data) { + setContent(res.data.content || ''); + // setTags(res.data.tags || []); // 已移除tags + } else { + toast({ title: '获取失败', description: res?.msg || '获取素材详情失败', variant: 'destructive' }); + } + }) + .catch(error => { + toast({ title: '网络错误', description: error?.message || '请检查网络连接', variant: 'destructive' }); + }) + // .finally(() => setLoading(false)); // 已移除loading + } else { + setIsEdit(false); + setContent(''); + // setTags([]); // 已移除tags + } + }, [materialId]); + + // 移除未用的Badge、Plus、newTag、setNewTag、tags、setTags、loading、setResUrls、handleAddTag、handleRemoveTag + // 保证所有变量和set方法都已声明 + // 定义内容类型常量和类型 + const MATERIAL_TYPE_OPTIONS = [ + { id: 1, name: '图片' }, + { id: 2, name: '链接' }, + { id: 3, name: '视频' }, + { id: 4, name: '文本' }, + { id: 5, name: '小程序' }, + ]; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!content) { + toast({ + title: '错误', + description: '请输入素材内容', + variant: 'destructive', + }); + return; + } + setIsSubmitting(true); + try { + let res; + if (isEdit) { + // 编辑模式,调用新接口 + const payload = { + id: materialId, + contentType: 4, + content, + comment: '', + sendTime: '', + }; + res = await put('/v1/content/library/update-item', payload); + } else { + // 新建模式,调用新接口 + const payload = { + libraryId: id, + type: 1, + content, + comment, + sendTime, + resUrls, + }; + res = await post('/v1/content/library/create-item', payload); + } + if (res && res.code === 200) { + toast({ title: '成功', description: isEdit ? '素材已更新' : '新素材已创建' }); + navigate(-1); + } else { + toast({ title: isEdit ? '保存失败' : '创建失败', description: res?.msg || (isEdit ? '保存素材失败' : '创建新素材失败'), variant: 'destructive' }); + } + } catch (error: any) { + toast({ title: '网络错误', description: error?.message || '请检查网络连接', variant: 'destructive' }); + } finally { + setIsSubmitting(false); + } + }; + + // 上传图片模拟 + const handleUploadImage = () => { + // 这里应对接真实上传逻辑 + const mock = [ + 'https://picsum.photos/id/237/200/300', + 'https://picsum.photos/id/238/200/300', + 'https://picsum.photos/id/239/200/300', + ]; + const random = mock[Math.floor(Math.random() * mock.length)]; + if (!images.includes(random)) setImages([...images, random]); + }; + const handleRemoveImage = (idx: number) => { + setImages(images.filter((_, i) => i !== idx)); + }; + + return ( + navigate(-1)} />} + > +
+
+ +
+ {/* 基础信息分组 */} +
+
基础信息
+ + setSendTime(e.target.value)} + className="w-full h-12 rounded-2xl border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 px-4 text-base placeholder:text-gray-300" + placeholder="请选择发布时间" + /> + + +
+ {/* 内容信息分组 */} +
+
内容信息
+ +