feat: 本次提交更新内容如下
新建素材操作了
This commit is contained in:
@@ -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() {
|
||||
<Route path="/contact-import" element={<ContactImport />} />
|
||||
<Route path="/content" element={<Content />} />
|
||||
<Route path="/content/new" element={<NewContent />} />
|
||||
<Route path="/content/edit/:id" element={<NewContent />} />
|
||||
<Route path="/content/materials/:id" element={<Materials />} />
|
||||
<Route path="/content/materials/new/:id" element={<MaterialsNew />} />
|
||||
<Route path="/content/materials/edit/:id/:materialId" element={<MaterialsNew />} />
|
||||
{/* 你可以继续添加更多路由 */}
|
||||
</Routes>
|
||||
</LayoutWrapper>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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<ContentLibraryForm>({
|
||||
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 (
|
||||
<Layout
|
||||
header={<UnifiedHeader title="新建内容库" showBack onBack={() => navigate(-1)} />}
|
||||
header={<UnifiedHeader title={isEdit ? "编辑内容库" : "新建内容库"} showBack onBack={() => navigate(-1)} />}
|
||||
footer={
|
||||
<div className="p-4">
|
||||
<Button theme="primary" block onClick={handleSave} disabled={isSubmitting || !form.name} >
|
||||
{isSubmitting ? '创建中...' : '创建内容库'}
|
||||
{isSubmitting ? (isEdit ? '保存中...' : '创建中...') : (isEdit ? '保存' : '创建内容库')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
192
nkebao/src/pages/content/materials/List.tsx
Normal file
192
nkebao/src/pages/content/materials/List.tsx
Normal file
@@ -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<MaterialItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [aiDialogOpen, setAiDialogOpen] = useState(false);
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<MaterialItem | null>(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 (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<UnifiedHeader title="素材列表" showBack onBack={() => navigate(-1)}
|
||||
|
||||
rightContent={
|
||||
<>
|
||||
<Button onClick={handleNewMaterial} variant="default">
|
||||
<Plus className="h-4 w-4 mr-1" />新建素材
|
||||
</Button>
|
||||
</>
|
||||
}/>
|
||||
<div className="flex items-center gap-2 m-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索素材内容或标签..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSearch(); }}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} variant="outline">搜索</Button>
|
||||
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen pb-16">
|
||||
<div className="p-4 space-y-4 max-w-2xl mx-auto">
|
||||
<div className="space-y-2">
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-400">加载中...</div>
|
||||
) : materials.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">暂无素材</div>
|
||||
) : (
|
||||
materials.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5 mb-4 flex flex-col"
|
||||
style={{ boxShadow: '0 2px 8px 0 rgba(0,0,0,0.04)' }}
|
||||
>
|
||||
{/* 顶部头像+系统创建+ID */}
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-2xl mr-3">
|
||||
<UserCircle2 className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-base text-gray-800 leading-tight">系统创建</span>
|
||||
<span className="mt-1">
|
||||
<span className="bg-blue-50 text-blue-700 text-xs font-bold rounded-full px-3 py-0.5 align-middle">ID: {item.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 标题 */}
|
||||
<div className="font-bold text-lg text-gray-900 mb-2 mt-1">{item.title ? `【${item.title}】` : (item.content.length > 20 ? `【${item.content.slice(0, 20)}...】` : `【${item.content}】`)}</div>
|
||||
{/* 内容 */}
|
||||
<div className="text-base text-gray-800 whitespace-pre-line mb-3" style={{ lineHeight: '1.8' }}>{item.content}</div>
|
||||
{/* 标签 */}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{item.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
<Tag className="h-3 w-3 mr-1" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 操作按钮区 */}
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => handleEdit(item.id)}>
|
||||
<Edit className="h-4 w-4 mr-1" />编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => { setSelectedMaterial(item); setAiDialogOpen(true); }}>
|
||||
<BarChart className="h-4 w-4 mr-1" />AI分析
|
||||
</Button>
|
||||
<Dialog open={aiDialogOpen && selectedMaterial?.id === item.id} onOpenChange={setAiDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI 分析结果</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<p>{selectedMaterial?.aiAnalysis || '正在分析中...'}</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<Button size="sm" variant="destructive" onClick={() => handleDelete(item.id)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
285
nkebao/src/pages/content/materials/New.tsx
Normal file
285
nkebao/src/pages/content/materials/New.tsx
Normal file
@@ -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<number>(1);
|
||||
const [desc, setDesc] = useState('');
|
||||
const [coverImage, setCoverImage] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
const [resUrls, setResUrls] = useState<string[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [sendTime, setSendTime] = useState('');
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
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 (
|
||||
<Layout
|
||||
header={<UnifiedHeader title={isEdit ? '编辑素材' : '新建素材'} showBack onBack={() => navigate(-1)} />}
|
||||
>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
<div className="p-4 max-w-lg mx-auto">
|
||||
<Card className="p-8 rounded-3xl shadow-xl bg-white">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* 基础信息分组 */}
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-gray-400 mb-2 tracking-widest">基础信息</div>
|
||||
<Label className="font-bold flex items-center mb-2">发布时间</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={sendTime}
|
||||
onChange={e => 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="请选择发布时间"
|
||||
/>
|
||||
<Label className="font-bold flex items-center mb-2 mt-4"><span className="text-red-500 mr-1">*</span>类型</Label>
|
||||
<select
|
||||
value={contentType}
|
||||
onChange={e => setContentType(Number(e.target.value))}
|
||||
className="w-full h-12 rounded-2xl border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 px-4 text-base bg-white appearance-none"
|
||||
>
|
||||
<option value="" disabled>请选择类型</option>
|
||||
<option value={1}>图片</option>
|
||||
<option value={2}>链接</option>
|
||||
<option value={3}>视频</option>
|
||||
<option value={4}>文本</option>
|
||||
<option value={5}>小程序</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* 内容信息分组 */}
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-gray-400 mb-2 tracking-widest">内容信息</div>
|
||||
<Label htmlFor="content" className="font-bold flex items-center mb-2"><span className="text-red-500 mr-1">*</span>内容</Label>
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
placeholder="请输入内容"
|
||||
className="w-full rounded-2xl border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 px-4 text-base min-h-[120px] bg-gray-50 placeholder:text-gray-300"
|
||||
rows={8}
|
||||
/>
|
||||
{(contentType === 2 || contentType === 6) && (
|
||||
<>
|
||||
<Label htmlFor="desc" className="font-bold flex items-center mb-2"><span className="text-red-500 mr-1">*</span>描述</Label>
|
||||
<Input
|
||||
id="desc"
|
||||
value={desc}
|
||||
onChange={e => setDesc(e.target.value)}
|
||||
placeholder="请输入描述"
|
||||
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"
|
||||
/>
|
||||
<Label className="font-bold mb-2 mt-4">封面图</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
{!coverImage ? (
|
||||
<Button type="button" variant="outline" className="rounded-2xl border-dashed border-2 border-blue-300 bg-white hover:bg-blue-50 h-28 w-28 flex flex-col items-center justify-center p-0" onClick={() => setCoverImage('https://cdn-icons-png.flaticon.com/512/732/732212.png')}>
|
||||
<UploadCloud className="h-8 w-8 mb-2 text-gray-400 mx-auto" />
|
||||
<span className="text-sm text-gray-500">上传封面图</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<img src={coverImage} alt="封面图" className="object-contain rounded-xl mx-auto my-auto w-20 h-20" />
|
||||
<Button type="button" variant="destructive" size="sm" className="absolute top-1 right-1 h-8 px-2 rounded-lg" onClick={() => setCoverImage('')}>删除</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Label htmlFor="url" className="font-bold flex items-center mb-2 mt-4"><span className="text-red-500 mr-1">*</span>链接地址</Label>
|
||||
<Input
|
||||
id="url"
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
placeholder="请输入链接地址"
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{contentType === 3 && (
|
||||
<>
|
||||
<Label className="font-bold mb-2">上传视频</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
{!videoUrl ? (
|
||||
<Button type="button" variant="outline" className="rounded-2xl border-dashed border-2 border-blue-300 bg-white hover:bg-blue-50 h-28 w-44 flex flex-col items-center justify-center p-0" onClick={() => setVideoUrl('https://www.w3schools.com/html/mov_bbb.mp4')}>
|
||||
<UploadCloud className="h-8 w-8 mb-2 text-gray-400 mx-auto" />
|
||||
<span className="text-sm text-gray-500">上传视频</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<video src={videoUrl} controls className="object-contain rounded-xl h-24 w-40 mx-auto my-auto" />
|
||||
<Button type="button" variant="destructive" size="sm" className="absolute top-1 right-1 h-8 px-2 rounded-lg" onClick={() => setVideoUrl('')}>删除</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">支持MP4,建议不超过20MB</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 素材上传分组(仅图片类型和小程序类型) */}
|
||||
{(contentType === 1 || contentType === 5) && (
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-gray-400 mb-2 tracking-widest">素材上传</div>
|
||||
{contentType === 1 && (
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-gray-400 mb-2 tracking-widest">素材上传</div>
|
||||
<Upload
|
||||
files={images.map(url => ({ url }))}
|
||||
onChange={({ files }) => setImages(files.map(f => f.url))}
|
||||
multiple
|
||||
accept="image/*"
|
||||
max={9}
|
||||
theme="image"
|
||||
tips="支持JPG、PNG格式,最多9张"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{contentType === 5 && (
|
||||
<div className="space-y-6">
|
||||
<Label htmlFor="appTitle" className="font-bold mb-2">小程序名称</Label>
|
||||
<Input id="appTitle" placeholder="请输入小程序名称" 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" />
|
||||
<Label htmlFor="appId" className="font-bold mb-2">AppID</Label>
|
||||
<Input id="appId" placeholder="请输入AppID" 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" />
|
||||
<Label className="font-bold mb-2">小程序封面图</Label>
|
||||
<div className="border border-dashed border-gray-300 rounded-2xl p-4 text-center bg-gray-50">
|
||||
<Button type="button" variant="outline" onClick={handleUploadImage} className="w-full py-4 flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-blue-300 bg-white hover:bg-blue-50">
|
||||
<UploadCloud className="h-6 w-6 mb-2 text-gray-400" />
|
||||
<span>上传小程序封面图</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 评论/备注分组 */}
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-gray-400 mb-2 tracking-widest">评论/备注</div>
|
||||
<Textarea
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
placeholder="请输入评论或备注"
|
||||
className="w-full rounded-2xl border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 px-4 text-base min-h-[80px] bg-gray-50 placeholder:text-gray-300"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
{/* 底部主按钮 */}
|
||||
<Button type="submit" className="w-full h-12 rounded-2xl bg-blue-600 hover:bg-blue-700 text-base font-bold mt-12 shadow">
|
||||
{isSubmitting ? (isEdit ? '保存中...' : '创建中...') : (isEdit ? '保存修改' : '保存素材')}
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user