feat: 本次提交更新内容如下
定版本转移2025年7月17日
This commit is contained in:
@@ -1,382 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronLeft, Filter, Search, RefreshCw, Plus, Edit, Trash2, Eye, MoreVertical, Copy } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { get, del } from '@/api/request';
|
||||
import Layout from '@/components/Layout';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface LibraryListResponse {
|
||||
list: ContentLibrary[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface WechatGroupMember {
|
||||
id: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
avatar: string;
|
||||
gender?: 'male' | 'female';
|
||||
role?: 'owner' | 'admin' | 'member';
|
||||
joinTime?: string;
|
||||
}
|
||||
|
||||
interface ContentLibrary {
|
||||
id: string;
|
||||
name: string;
|
||||
source: 'friends' | 'groups';
|
||||
targetAudience: {
|
||||
id: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
}[];
|
||||
creator: string;
|
||||
creatorName?: string;
|
||||
itemCount: number;
|
||||
lastUpdated: string;
|
||||
enabled: boolean;
|
||||
sourceFriends: string[];
|
||||
sourceGroups: string[];
|
||||
friendsData?: any[];
|
||||
groupsData?: any[];
|
||||
keywordInclude: string[];
|
||||
keywordExclude: string[];
|
||||
isEnabled: number;
|
||||
aiPrompt: string;
|
||||
timeEnabled: number;
|
||||
timeStart: string;
|
||||
timeEnd: string;
|
||||
status: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
sourceType: number;
|
||||
selectedGroupMembers?: WechatGroupMember[];
|
||||
}
|
||||
|
||||
function CardMenu({ onView, onEdit, onDelete, onViewMaterials }: {
|
||||
onView: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onViewMaterials: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const menuRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
if (open) document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setOpen((v) => !v)} style={{ background: "none", border: "none", padding: 0, margin: 0, cursor: "pointer" }}>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 28,
|
||||
background: "#fff",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||
zIndex: 100,
|
||||
minWidth: 120,
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
<div onClick={() => { onEdit(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Edit className="h-4 w-4 mr-2" />编辑
|
||||
</div>
|
||||
<div onClick={() => { onDelete(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, color: "#e53e3e", transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />删除
|
||||
</div>
|
||||
<div onClick={() => { onViewMaterials(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Eye className="h-4 w-4 mr-2" />查看素材
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Content() {
|
||||
const navigate = useNavigate();
|
||||
const [libraries, setLibraries] = useState<ContentLibrary[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// 获取内容库列表
|
||||
const fetchLibraries = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: '1',
|
||||
limit: '100',
|
||||
...(searchQuery ? { keyword: searchQuery } : {}),
|
||||
...(activeTab !== 'all' ? { sourceType: activeTab === 'friends' ? '1' : '2' } : {})
|
||||
});
|
||||
const response = await get<ApiResponse<LibraryListResponse>>(`/v1/content/library/list?${queryParams.toString()}`);
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
// 转换数据格式以匹配原有UI
|
||||
const transformedLibraries = response.data.list.map((item: any) => {
|
||||
const friendsData = Array.isArray(item.selectedFriends) ? item.selectedFriends : [];
|
||||
const groupsData = Array.isArray(item.selectedGroups) ? item.selectedGroups : [];
|
||||
|
||||
const transformedItem: ContentLibrary = {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
source: item.sourceType === 1 ? 'friends' : 'groups',
|
||||
targetAudience: [
|
||||
...friendsData.map((friend: any) => ({
|
||||
id: friend.id,
|
||||
nickname: friend.nickname || `好友${friend.id}`,
|
||||
avatar: friend.avatar || '/placeholder.svg'
|
||||
})),
|
||||
...groupsData.map((group: any) => ({
|
||||
id: group.id,
|
||||
nickname: group.name || `群组${group.id}`,
|
||||
avatar: group.avatar || '/placeholder.svg'
|
||||
}))
|
||||
],
|
||||
creator: item.creatorName || '系统',
|
||||
creatorName: item.creatorName,
|
||||
itemCount: item.itemCount,
|
||||
lastUpdated: item.updateTime,
|
||||
enabled: item.isEnabled === 1,
|
||||
sourceFriends: item.sourceFriends || [],
|
||||
sourceGroups: item.sourceGroups || [],
|
||||
friendsData: friendsData,
|
||||
groupsData: groupsData,
|
||||
keywordInclude: item.keywordInclude || [],
|
||||
keywordExclude: item.keywordExclude || [],
|
||||
isEnabled: item.isEnabled,
|
||||
aiPrompt: item.aiPrompt || '',
|
||||
timeEnabled: item.timeEnabled,
|
||||
timeStart: item.timeStart || '',
|
||||
timeEnd: item.timeEnd || '',
|
||||
status: item.status,
|
||||
createTime: item.createTime,
|
||||
updateTime: item.updateTime,
|
||||
sourceType: item.sourceType,
|
||||
selectedGroupMembers: item.selectedGroupMembers || []
|
||||
};
|
||||
return transformedItem;
|
||||
});
|
||||
setLibraries(transformedLibraries);
|
||||
} else {
|
||||
toast({ title: '获取失败', description: response.msg || '获取内容库列表失败' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取内容库列表失败:', error);
|
||||
toast({ title: '网络错误', description: error?.message || '请检查网络连接' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchQuery, activeTab, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLibraries();
|
||||
}, [fetchLibraries]);
|
||||
|
||||
const handleCreateNew = () => {
|
||||
navigate('/content/new');
|
||||
};
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
navigate(`/content/edit/${id}`);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const response = await del<ApiResponse>(`/v1/content/library/delete?id=${id}`);
|
||||
if (response.code === 200) {
|
||||
toast({ title: '删除成功', description: '内容库已删除' });
|
||||
fetchLibraries();
|
||||
} else {
|
||||
toast({ title: '删除失败', description: response.msg || '删除失败' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('删除内容库失败:', error);
|
||||
toast({ title: '网络错误', description: error?.message || '请检查网络连接' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewMaterials = (id: string) => {
|
||||
navigate(`/content/materials/${id}`);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchLibraries();
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLibraries();
|
||||
};
|
||||
|
||||
const filteredLibraries = libraries.filter(
|
||||
(library) =>
|
||||
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
library.targetAudience.some((target) => target.nickname.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<UnifiedHeader title="内容库" showBack />
|
||||
<div className="bg-white shadow-sm rounded-b-xl px-4 pt-4 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-9 rounded-full bg-gray-50 border-none focus:ring-2 focus:ring-blue-100"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className="rounded-full border-gray-200"
|
||||
>
|
||||
<RefreshCw className={`h-5 w-5 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<Button onClick={handleCreateNew} className="rounded-full px-4 py-2" size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />新建
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3 rounded-full bg-gray-100">
|
||||
<TabsTrigger value="all" className="rounded-full">全部</TabsTrigger>
|
||||
<TabsTrigger value="friends" className="rounded-full">微信好友</TabsTrigger>
|
||||
<TabsTrigger value="groups" className="rounded-full">聊天群</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="space-y-3">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<RefreshCw className="h-8 w-8 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
) : filteredLibraries.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
|
||||
<img src="/empty-state-content.svg" alt="暂无内容库" className="w-32 h-32 mb-4 opacity-80" />
|
||||
<div className="mb-2">暂无内容库,快去新建一个吧!</div>
|
||||
<Button onClick={handleCreateNew} size="sm" className="rounded-full px-6">新建内容库</Button>
|
||||
</div>
|
||||
) : (
|
||||
filteredLibraries.map((library, idx) => (
|
||||
<Card
|
||||
key={library.id}
|
||||
className={`p-4 rounded-xl shadow-sm border border-gray-100 transition hover:shadow-md bg-white ${idx !== filteredLibraries.length - 1 ? 'mb-2' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium text-base text-gray-900">{library.name}</h3>
|
||||
<Badge variant={library.isEnabled === 1 ? 'default' : 'secondary'} className="text-xs rounded-full px-2">
|
||||
{library.isEnabled === 1 ? '已启用' : '未启用'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>来源:</span>
|
||||
{library.sourceType === 1 && library.sourceFriends?.length > 0 ? (
|
||||
<div className="flex -space-x-1 overflow-hidden">
|
||||
{(library.friendsData || []).slice(0, 3).map((friend) => (
|
||||
<img
|
||||
key={friend.id}
|
||||
src={friend.avatar || '/placeholder.svg'}
|
||||
alt={friend.nickname || `好友${friend.id}`}
|
||||
className="inline-block h-6 w-6 rounded-full ring-2 ring-white"
|
||||
/>
|
||||
))}
|
||||
{library.sourceFriends.length > 3 && (
|
||||
<span className="flex items-center justify-center h-6 w-6 rounded-full bg-gray-200 text-xs font-medium text-gray-800">
|
||||
+{library.sourceFriends.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : library.sourceType === 2 && library.sourceGroups?.length > 0 ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex -space-x-1 overflow-hidden">
|
||||
{(library.groupsData || []).slice(0, 3).map((group) => (
|
||||
<img
|
||||
key={group.id}
|
||||
src={group.avatar || '/placeholder.svg'}
|
||||
alt={group.name || `群组${group.id}`}
|
||||
className="inline-block h-6 w-6 rounded-full ring-2 ring-white"
|
||||
/>
|
||||
))}
|
||||
{library.sourceGroups.length > 3 && (
|
||||
<span className="flex items-center justify-center h-6 w-6 rounded-full bg-gray-200 text-xs font-medium text-gray-800">
|
||||
+{library.sourceGroups.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-6 h-6 bg-gray-200 rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
<div>创建人:{library.creator}</div>
|
||||
<div>内容数量:{library.itemCount}</div>
|
||||
<div>更新时间:{new Date(library.updateTime).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardMenu
|
||||
onView={() => navigate(`/content/${library.id}`)}
|
||||
onEdit={() => handleEdit(library.id)}
|
||||
onDelete={() => handleDelete(library.id)}
|
||||
onViewMaterials={() => handleViewMaterials(library.id)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
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 { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
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 { get, post } from "@/api/request";
|
||||
// TODO: 引入微信好友/群组选择器、日期选择器等组件
|
||||
|
||||
interface WechatFriend {
|
||||
id: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
}
|
||||
interface WechatGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
interface ContentLibraryForm {
|
||||
name: string;
|
||||
sourceType: "friends" | "groups";
|
||||
keywordsInclude: string;
|
||||
keywordsExclude: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedFriends: WechatFriend[];
|
||||
selectedGroups: WechatGroup[];
|
||||
useAI: boolean;
|
||||
aiPrompt: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export default function NewContentLibraryPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const isEdit = !!id;
|
||||
const [form, setForm] = useState<ContentLibraryForm>({
|
||||
name: "",
|
||||
sourceType: "friends",
|
||||
keywordsInclude: "",
|
||||
keywordsExclude: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
selectedFriends: [],
|
||||
selectedGroups: [],
|
||||
useAI: false,
|
||||
aiPrompt: "",
|
||||
enabled: true,
|
||||
});
|
||||
const [selectedFriendObjs, setSelectedFriendObjs] = useState<WechatFriend[]>(
|
||||
[]
|
||||
);
|
||||
const [selectedGroupObjs, setSelectedGroupObjs] = useState<WechatGroup[]>([]);
|
||||
const [isFriendSelectorOpen, setIsFriendSelectorOpen] = useState(false);
|
||||
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)),
|
||||
groups: form.selectedGroups.map((g) => Number(g.id)),
|
||||
groupMembers: {},
|
||||
keywordInclude: form.keywordsInclude
|
||||
? form.keywordsInclude
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
keywordExclude: form.keywordsExclude
|
||||
? form.keywordsExclude
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
aiPrompt: form.aiPrompt,
|
||||
timeEnabled: form.startDate || form.endDate ? 1 : 0,
|
||||
startTime: form.startDate || "",
|
||||
endTime: form.endDate || "",
|
||||
status: form.enabled ? 1 : 0,
|
||||
};
|
||||
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: isEdit ? "保存失败" : "创建失败",
|
||||
description: "保存内容库失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<UnifiedHeader
|
||||
title={isEdit ? "编辑内容库" : "新建内容库"}
|
||||
showBack
|
||||
onBack={() => navigate(-1)}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<div className="p-4">
|
||||
<Button
|
||||
theme="primary"
|
||||
block
|
||||
onClick={handleSave}
|
||||
disabled={isSubmitting || !form.name}
|
||||
>
|
||||
{isSubmitting
|
||||
? isEdit
|
||||
? "保存中..."
|
||||
: "创建中..."
|
||||
: isEdit
|
||||
? "保存"
|
||||
: "创建内容库"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex-1 bg-gray-50 ">
|
||||
<div className="p-4 space-y-4 max-w-lg mx-auto">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block font-medium mb-1">
|
||||
内容库名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, name: e.target.value }))
|
||||
}
|
||||
placeholder="请输入内容库名称"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">数据来源配置</label>
|
||||
<Tabs
|
||||
value={form.sourceType}
|
||||
onValueChange={(val) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
sourceType: val as "friends" | "groups",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="friends">选择微信好友</TabsTrigger>
|
||||
<TabsTrigger value="groups">选择聊天群</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="friends">
|
||||
<FriendSelection
|
||||
selectedFriends={form.selectedFriends.map((f) => f.id)}
|
||||
onSelect={(ids) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
selectedFriends: ids.map((id) => ({
|
||||
id,
|
||||
nickname: id,
|
||||
avatar: "",
|
||||
})),
|
||||
}))
|
||||
}
|
||||
onSelectDetail={setSelectedFriendObjs}
|
||||
enableDeviceFilter={false}
|
||||
placeholder="选择微信好友"
|
||||
/>
|
||||
{selectedFriendObjs.length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{selectedFriendObjs.map((friend) => (
|
||||
<div
|
||||
key={friend.id}
|
||||
className="flex items-center justify-between bg-gray-100 p-2 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{friend.avatar ? (
|
||||
<img
|
||||
src={friend.avatar}
|
||||
alt={friend.nickname}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-white text-sm">
|
||||
{friend.nickname?.charAt(0) || "友"}
|
||||
</div>
|
||||
)}
|
||||
<span>{friend.nickname}</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-gray-400 hover:text-red-500 ml-2"
|
||||
onClick={() => {
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
selectedFriends: f.selectedFriends.filter(
|
||||
(frd) => frd.id !== friend.id
|
||||
),
|
||||
}));
|
||||
setSelectedFriendObjs((objs) =>
|
||||
objs.filter((frd) => frd.id !== friend.id)
|
||||
);
|
||||
}}
|
||||
title="移除"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="groups">
|
||||
<GroupSelection
|
||||
selectedGroups={form.selectedGroups.map((g) => g.id)}
|
||||
onSelect={(ids) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
selectedGroups: ids.map((id) => {
|
||||
const old = f.selectedGroups.find(
|
||||
(g) => g.id === id
|
||||
);
|
||||
return old || { id, name: id, avatar: "" };
|
||||
}),
|
||||
}))
|
||||
}
|
||||
onSelectDetail={setSelectedGroupObjs}
|
||||
placeholder="选择群聊"
|
||||
/>
|
||||
{selectedGroupObjs.length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{selectedGroupObjs.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className="flex items-center justify-between bg-gray-100 p-2 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{group.avatar ? (
|
||||
<img
|
||||
src={group.avatar}
|
||||
alt={group.name}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-white text-sm">
|
||||
{group.name?.charAt(0) || "群"}
|
||||
</div>
|
||||
)}
|
||||
<span>{group.name}</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-gray-400 hover:text-red-500 ml-2"
|
||||
onClick={() => {
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
selectedGroups: f.selectedGroups.filter(
|
||||
(grp) => grp.id !== group.id
|
||||
),
|
||||
}));
|
||||
setSelectedGroupObjs((objs) =>
|
||||
objs.filter((grp) => grp.id !== group.id)
|
||||
);
|
||||
}}
|
||||
title="移除"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Collapse>
|
||||
<CollapsePanel header="关键字设置" value="keywords">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block font-medium mb-1">
|
||||
关键字匹配
|
||||
</label>
|
||||
<Textarea
|
||||
value={form.keywordsInclude}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
keywordsInclude: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="如果设置了关键字,系统只会采集含有关键字的内容。多个关键字,用半角的','隔开。"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">
|
||||
关键字排除
|
||||
</label>
|
||||
<Textarea
|
||||
value={form.keywordsExclude}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
keywordsExclude: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="排除含有这些关键字的内容。多个关键字,用半角的','隔开。"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsePanel>
|
||||
</Collapse>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="block font-medium">是否启用AI</label>
|
||||
</div>
|
||||
<div className="w-10">
|
||||
<Switch
|
||||
checked={form.useAI}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((f) => ({ ...f, useAI: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1 ">
|
||||
当启用AI之后,该内容库下的所有内容,都会通过AI重新生成内容。
|
||||
</p>
|
||||
{form.useAI && (
|
||||
<div>
|
||||
<label className="block font-medium mb-1">AI 提示词</label>
|
||||
<Textarea
|
||||
value={form.aiPrompt}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, aiPrompt: e.target.value }))
|
||||
}
|
||||
placeholder="请输入 AI 提示词"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block font-medium mb-2">时间限制</label>
|
||||
{/* TODO: 替换为TDesign日期范围选择器 */}
|
||||
<div
|
||||
className="flex mb-2"
|
||||
style={{ justifyContent: "space-between" }}
|
||||
>
|
||||
<label className="text-sm w-20 ">开始时间</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={form.startDate}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, startDate: e.target.value }))
|
||||
}
|
||||
className="inline-block w-1/2 "
|
||||
/>
|
||||
</div>
|
||||
<div className="flex ">
|
||||
<label className="text-sm w-20">结束时间</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={form.endDate}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, endDate: e.target.value }))
|
||||
}
|
||||
className="inline-block w-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block font-medium mb-1">是否启用</label>
|
||||
<Switch
|
||||
checked={form.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((f) => ({ ...f, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{/* TODO: 微信好友/群组选择器弹窗、日期选择器弹窗 */}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
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 { 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
|
||||
resUrls?: string[]; // Added for image URLs
|
||||
}
|
||||
|
||||
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.resUrls && item.resUrls.length > 0 && (
|
||||
<div className="flex flex-col gap-2 mb-3">
|
||||
{item.resUrls.map((url: string, idx: number) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={url}
|
||||
alt="素材图片"
|
||||
className="w-full max-w-full rounded-lg border"
|
||||
style={{ height: 'auto', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from 'tdesign-mobile-react';
|
||||
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 Layout from '@/components/Layout';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import { get, post } from '@/api/request';
|
||||
import UploadImage from '@/components/UploadImage';
|
||||
import UploadVideo from '@/components/UploadVideo';
|
||||
|
||||
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 [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [sendTime, setSendTime] = useState('');
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||
// 优化图片上传逻辑,确保每次选择图片后立即上传并回显
|
||||
|
||||
|
||||
// 判断模式并拉取详情
|
||||
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 || '');
|
||||
setComment(res.data.comment || '');
|
||||
setSendTime(res.data.sendTime || '');
|
||||
if (isFirstLoad && res.data.contentType) {
|
||||
setContentType(Number(res.data.contentType));
|
||||
setIsFirstLoad(false);
|
||||
}
|
||||
setDesc(res.data.desc || '');
|
||||
setCoverImage(res.data.coverImage || '');
|
||||
setUrl(res.data.url || '');
|
||||
setVideoUrl(res.data.videoUrl || '');
|
||||
setImages(res.data.resUrls || []); // 图片回显
|
||||
} else {
|
||||
toast({ title: '获取失败', description: res?.msg || '获取素材详情失败', variant: 'destructive' });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast({ title: '网络错误', description: error?.message || '请检查网络连接', variant: 'destructive' });
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
setContent('');
|
||||
setComment('');
|
||||
setSendTime('');
|
||||
setContentType(1);
|
||||
setImages([]);
|
||||
setIsFirstLoad(true);
|
||||
}
|
||||
}, [materialId]);
|
||||
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!content) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '请输入素材内容',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
let res;
|
||||
if (isEdit) {
|
||||
// 编辑模式,调用新接口,所有字段取表单值
|
||||
const payload = {
|
||||
id: materialId,
|
||||
contentType,
|
||||
content,
|
||||
comment,
|
||||
sendTime,
|
||||
resUrls: images,
|
||||
};
|
||||
res = await post('/v1/content/library/update-item', payload);
|
||||
} else {
|
||||
// 新建模式,所有字段取表单值
|
||||
const payload = {
|
||||
libraryId: id,
|
||||
type: contentType,
|
||||
content,
|
||||
comment,
|
||||
sendTime,
|
||||
resUrls: images,
|
||||
};
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 移除未用的 handleUploadImage 及 uploadImage 相关代码
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={<UnifiedHeader title={isEdit ? '编辑素材' : '新建素材'} showBack onBack={() => navigate(-1)} />}
|
||||
footer={
|
||||
<div className='m-2'>
|
||||
{/* 2. 按钮onClick绑定handleSave */}
|
||||
<Button theme="primary" block onClick={handleSave} disabled={isSubmitting}>
|
||||
{isSubmitting ? (isEdit ? '保存中...' : '创建中...') : (isEdit ? '保存修改' : '保存素材')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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 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 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">
|
||||
<UploadImage
|
||||
value={images}
|
||||
onChange={urls => {
|
||||
setCoverImage(urls[0]);
|
||||
}}
|
||||
max={1}
|
||||
accept="image/*"
|
||||
/>
|
||||
</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="pt-4">
|
||||
<UploadVideo
|
||||
value={videoUrl}
|
||||
onChange={setVideoUrl}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 素材上传分组(仅图片类型和小程序类型) */}
|
||||
{([1,5].includes(contentType)) && (
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-gray-400 mb-2 tracking-widest">素材上传(最多上传9张)</div>
|
||||
{contentType === 1 && (
|
||||
<div className="mb-6">
|
||||
<UploadImage
|
||||
value={images}
|
||||
onChange={urls => {
|
||||
setImages(urls);
|
||||
}}
|
||||
max={9}
|
||||
accept="image/*"
|
||||
/>
|
||||
</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>
|
||||
<UploadImage
|
||||
value={images}
|
||||
onChange={urls => {
|
||||
setImages(urls);
|
||||
}}
|
||||
max={9}
|
||||
accept="image/*"
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user