Merge branch 'yongpxu-dev' into yongpxu-master

This commit is contained in:
笔记本里的永平
2025-07-17 10:13:35 +08:00
6 changed files with 457 additions and 231 deletions

View File

@@ -41,18 +41,11 @@ export default function DeviceSelection({
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// 当弹窗打开时获取设备列表 // 获取设备列表支持keyword
useEffect(() => { const fetchDevices = async (keyword: string = "") => {
if (dialogOpen) {
fetchDevices();
}
}, [dialogOpen]);
// 获取设备列表
const fetchDevices = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await fetchDeviceList(1, 100); const res = await fetchDeviceList(1, 100, keyword.trim() || undefined);
if (res && res.data && Array.isArray(res.data.list)) { if (res && res.data && Array.isArray(res.data.list)) {
setDevices( setDevices(
res.data.list.map((d) => ({ res.data.list.map((d) => ({
@@ -71,19 +64,29 @@ export default function DeviceSelection({
} }
}; };
// 过滤设备 // 打开弹窗时获取设备列表
const filteredDevices = devices.filter((device) => { const openDialog = () => {
const matchesSearch = setSearchQuery("");
device.name.toLowerCase().includes(searchQuery.toLowerCase()) || setDialogOpen(true);
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) || fetchDevices("");
device.wechatId.toLowerCase().includes(searchQuery.toLowerCase()); };
// 搜索防抖
useEffect(() => {
if (!dialogOpen) return;
const timer = setTimeout(() => {
fetchDevices(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery, dialogOpen]);
// 过滤设备(只保留状态过滤)
const filteredDevices = devices.filter((device) => {
const matchesStatus = const matchesStatus =
statusFilter === "all" || statusFilter === "all" ||
(statusFilter === "online" && device.status === "online") || (statusFilter === "online" && device.status === "online") ||
(statusFilter === "offline" && device.status === "offline"); (statusFilter === "offline" && device.status === "offline");
return matchesStatus;
return matchesSearch && matchesStatus;
}); });
// 处理设备选择 // 处理设备选择
@@ -110,7 +113,7 @@ export default function DeviceSelection({
placeholder={placeholder} placeholder={placeholder}
className="pl-10 h-14 rounded-xl border-gray-200 text-base" className="pl-10 h-14 rounded-xl border-gray-200 text-base"
readOnly readOnly
onClick={() => setDialogOpen(true)} onClick={openDialog}
value={getDisplayText()} value={getDisplayText()}
/> />
</div> </div>

View File

@@ -42,11 +42,16 @@ export function DeviceSelectionDialog({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [devices, setDevices] = useState<Device[]>([]); const [devices, setDevices] = useState<Device[]>([]);
// 获取设备列表 // 获取设备列表支持keyword
const fetchDevices = useCallback(async () => { const fetchDevices = useCallback(
async (keyword: string = "") => {
setLoading(true); setLoading(true);
try { try {
const response = await fetchDeviceList(1, 100, searchQuery); const response = await fetchDeviceList(
1,
100,
keyword.trim() || undefined
);
if (response.code === 200 && response.data) { if (response.code === 200 && response.data) {
// 转换服务端数据格式为组件需要的格式 // 转换服务端数据格式为组件需要的格式
const convertedDevices: Device[] = response.data.list.map( const convertedDevices: Device[] = response.data.list.map(
@@ -78,26 +83,33 @@ export function DeviceSelectionDialog({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [searchQuery, toast]); },
[toast]
);
// 打开弹窗时获取设备列表
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchDevices(); fetchDevices("");
} }
}, [open, searchQuery, fetchDevices]); }, [open, fetchDevices]);
// 搜索防抖
useEffect(() => {
if (!open) return;
const timer = setTimeout(() => {
fetchDevices(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery, open, fetchDevices]);
// 过滤设备(只保留状态过滤)
const filteredDevices = devices.filter((device) => { const filteredDevices = devices.filter((device) => {
const matchesSearch =
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.wxid.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = const matchesStatus =
statusFilter === "all" || statusFilter === "all" ||
(statusFilter === "online" && device.status === "online") || (statusFilter === "online" && device.status === "online") ||
(statusFilter === "offline" && device.status === "offline"); (statusFilter === "offline" && device.status === "offline");
return matchesStatus;
return matchesSearch && matchesStatus;
}); });
const handleDeviceSelect = (deviceId: string) => { const handleDeviceSelect = (deviceId: string) => {
@@ -137,7 +149,7 @@ export function DeviceSelectionDialog({
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={fetchDevices} onClick={() => fetchDevices(searchQuery)}
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (

View File

@@ -3,12 +3,7 @@ import { Search, X } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { get } from "@/api/request"; import { get } from "@/api/request";
// 微信好友接口类型 // 微信好友接口类型
@@ -38,11 +33,12 @@ interface FriendsResponse {
}; };
} }
// 获取好友列表API函数 // 获取好友列表API函数 - 添加 keyword 参数
const fetchFriendsList = async (params: { const fetchFriendsList = async (params: {
page: number; page: number;
limit: number; limit: number;
deviceIds?: string[]; deviceIds?: string[];
keyword?: string;
}): Promise<FriendsResponse> => { }): Promise<FriendsResponse> => {
if (params.deviceIds && params.deviceIds.length === 0) { if (params.deviceIds && params.deviceIds.length === 0) {
return { return {
@@ -58,8 +54,12 @@ const fetchFriendsList = async (params: {
} }
const deviceIdsParam = params?.deviceIds?.join(",") || ""; const deviceIdsParam = params?.deviceIds?.join(",") || "";
const keywordParam = params?.keyword
? `&keyword=${encodeURIComponent(params.keyword)}`
: "";
return get<FriendsResponse>( return get<FriendsResponse>(
`/v1/friend?page=${params.page}&limit=${params.limit}&deviceIds=${deviceIdsParam}` `/v1/friend?page=${params.page}&limit=${params.limit}&deviceIds=${deviceIdsParam}${keywordParam}`
); );
}; };
@@ -94,20 +94,33 @@ export default function FriendSelection({
// 打开弹窗并请求第一页好友 // 打开弹窗并请求第一页好友
const openDialog = () => { const openDialog = () => {
setCurrentPage(1); setCurrentPage(1);
setSearchQuery(""); // 重置搜索关键词
setDialogOpen(true); setDialogOpen(true);
fetchFriends(1); fetchFriends(1, "");
}; };
// 当页码变化时,拉取对应页数据(弹窗已打开时) // 当页码变化时,拉取对应页数据(弹窗已打开时)
useEffect(() => { useEffect(() => {
if (dialogOpen && currentPage !== 1) { if (dialogOpen && currentPage !== 1) {
fetchFriends(currentPage); fetchFriends(currentPage, searchQuery);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]); }, [currentPage]);
// 获取好友列表API // 搜索防抖
const fetchFriends = async (page: number) => { useEffect(() => {
if (!dialogOpen) return;
const timer = setTimeout(() => {
setCurrentPage(1); // 重置到第一页
fetchFriends(1, searchQuery);
}, 500); // 500 防抖
return () => clearTimeout(timer);
}, [searchQuery, dialogOpen]);
// 获取好友列表API - 添加 keyword 参数
const fetchFriends = async (page: number, keyword: string = "") => {
setLoading(true); setLoading(true);
try { try {
let res; let res;
@@ -119,9 +132,18 @@ export default function FriendSelection({
setLoading(false); setLoading(false);
return; return;
} }
res = await fetchFriendsList({ page, limit: 20, deviceIds: deviceIds }); res = await fetchFriendsList({
page,
limit: 20,
deviceIds: deviceIds,
keyword: keyword.trim() || undefined,
});
} else { } else {
res = await fetchFriendsList({ page, limit: 20 }); res = await fetchFriendsList({
page,
limit: 20,
keyword: keyword.trim() || undefined,
});
} }
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
@@ -144,13 +166,6 @@ export default function FriendSelection({
} }
}; };
// 过滤好友
const filteredFriends = friends.filter(
(friend) =>
friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase())
);
// 处理好友选择 // 处理好友选择
const handleFriendToggle = (friendId: string) => { const handleFriendToggle = (friendId: string) => {
let newIds: string[]; let newIds: string[];
@@ -176,6 +191,13 @@ export default function FriendSelection({
setDialogOpen(false); setDialogOpen(false);
}; };
// 清空搜索
const handleClearSearch = () => {
setSearchQuery("");
setCurrentPage(1);
fetchFriends(1, "");
};
return ( return (
<> <>
{/* 输入框 */} {/* 输入框 */}
@@ -221,25 +243,27 @@ export default function FriendSelection({
className="pl-10 py-2 rounded-full border-gray-200" className="pl-10 py-2 rounded-full border-gray-200"
/> />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
{searchQuery && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full" className="absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full"
onClick={() => setSearchQuery("")} onClick={handleClearSearch}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
)}
</div> </div>
</div> </div>
<ScrollArea className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto h-[50vh]">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-gray-500">...</div> <div className="text-gray-500">...</div>
</div> </div>
) : filteredFriends.length > 0 ? ( ) : friends.length > 0 ? (
<div className="divide-y"> <div className="divide-y">
{filteredFriends.map((friend) => ( {friends.map((friend) => (
<label <label
key={friend.id} key={friend.id}
className="flex items-center px-6 py-4 hover:bg-gray-50 cursor-pointer" className="flex items-center px-6 py-4 hover:bg-gray-50 cursor-pointer"
@@ -288,11 +312,15 @@ export default function FriendSelection({
) : ( ) : (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-gray-500"> <div className="text-gray-500">
{deviceIds.length === 0 ? "请先选择设备" : "没有找到好友"} {deviceIds.length === 0
? "请先选择设备"
: searchQuery
? `没有找到包含"${searchQuery}"的好友`
: "没有找到好友"}
</div> </div>
</div> </div>
)} )}
</ScrollArea> </div>
<div className="border-t p-4 flex items-center justify-between bg-white"> <div className="border-t p-4 flex items-center justify-between bg-white">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">

View File

@@ -36,12 +36,17 @@ interface GroupsResponse {
}; };
} }
// 修改支持keyword参数
const fetchGroupsList = async (params: { const fetchGroupsList = async (params: {
page: number; page: number;
limit: number; limit: number;
keyword?: string;
}): Promise<GroupsResponse> => { }): Promise<GroupsResponse> => {
const keywordParam = params.keyword
? `&keyword=${encodeURIComponent(params.keyword)}`
: "";
return get<GroupsResponse>( return get<GroupsResponse>(
`/v1/chatroom?page=${params.page}&limit=${params.limit}` `/v1/chatroom?page=${params.page}&limit=${params.limit}${keywordParam}`
); );
}; };
@@ -71,23 +76,38 @@ export default function GroupSelection({
// 打开弹窗并请求第一页群组 // 打开弹窗并请求第一页群组
const openDialog = () => { const openDialog = () => {
setCurrentPage(1); setCurrentPage(1);
setSearchQuery(""); // 重置搜索关键词
setDialogOpen(true); setDialogOpen(true);
fetchGroups(1); fetchGroups(1, "");
}; };
// 当页码变化时,拉取对应页数据(弹窗已打开时) // 当页码变化时,拉取对应页数据(弹窗已打开时)
useEffect(() => { useEffect(() => {
if (dialogOpen && currentPage !== 1) { if (dialogOpen && currentPage !== 1) {
fetchGroups(currentPage); fetchGroups(currentPage, searchQuery);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]); }, [currentPage]);
// 获取群组列表API // 搜索防抖
const fetchGroups = async (page: number) => { useEffect(() => {
if (!dialogOpen) return;
const timer = setTimeout(() => {
setCurrentPage(1);
fetchGroups(1, searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery, dialogOpen]);
// 获取群组列表API - 支持keyword
const fetchGroups = async (page: number, keyword: string = "") => {
setLoading(true); setLoading(true);
try { try {
const res = await fetchGroupsList({ page, limit: 20 }); const res = await fetchGroupsList({
page,
limit: 20,
keyword: keyword.trim() || undefined,
});
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
setGroups( setGroups(
res.data.list.map((group) => ({ res.data.list.map((group) => ({
@@ -110,13 +130,6 @@ export default function GroupSelection({
} }
}; };
// 过滤群组
const filteredGroups = groups.filter(
(group) =>
group.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
group.chatroomId.toLowerCase().includes(searchQuery.toLowerCase())
);
// 处理群组选择 // 处理群组选择
const handleGroupToggle = (groupId: string) => { const handleGroupToggle = (groupId: string) => {
let newIds: string[]; let newIds: string[];
@@ -142,6 +155,13 @@ export default function GroupSelection({
setDialogOpen(false); setDialogOpen(false);
}; };
// 清空搜索
const handleClearSearch = () => {
setSearchQuery("");
setCurrentPage(1);
fetchGroups(1, "");
};
return ( return (
<> <>
{/* 输入框 */} {/* 输入框 */}
@@ -186,25 +206,27 @@ export default function GroupSelection({
className="pl-10 py-2 rounded-full border-gray-200" className="pl-10 py-2 rounded-full border-gray-200"
/> />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
{searchQuery && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full" className="absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full"
onClick={() => setSearchQuery("")} onClick={handleClearSearch}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
)}
</div> </div>
</div> </div>
<ScrollArea className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto h-[50vh]">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-gray-500">...</div> <div className="text-gray-500">...</div>
</div> </div>
) : filteredGroups.length > 0 ? ( ) : groups.length > 0 ? (
<div className="divide-y"> <div className="divide-y">
{filteredGroups.map((group) => ( {groups.map((group) => (
<label <label
key={group.id} key={group.id}
className="flex items-center px-6 py-4 hover:bg-gray-50 cursor-pointer" className="flex items-center px-6 py-4 hover:bg-gray-50 cursor-pointer"
@@ -252,14 +274,19 @@ export default function GroupSelection({
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-gray-500"></div> <div className="text-gray-500">
{searchQuery
? `没有找到包含"${searchQuery}"的群聊`
: "没有找到群聊"}
</div>
</div> </div>
)} )}
</ScrollArea> </div>
<div className="border-t p-4 flex items-center justify-between bg-white"> <div className="border-t p-4 flex items-center justify-between bg-white">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{totalGroups} {totalGroups}
{searchQuery && ` (搜索: "${searchQuery}")`}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button

View File

@@ -1,25 +1,33 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from "react-router-dom";
import Layout from '@/components/Layout'; import Layout from "@/components/Layout";
import UnifiedHeader from '@/components/UnifiedHeader'; import UnifiedHeader from "@/components/UnifiedHeader";
import { Input } from '@/components/ui/input'; import { Input } from "@/components/ui/input";
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from "@/components/ui/textarea";
import { Switch } from '@/components/ui/switch'; import { Switch } from "@/components/ui/switch";
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Card } from '@/components/ui/card'; import { Card } from "@/components/ui/card";
import { Collapse, CollapsePanel ,Button} from 'tdesign-mobile-react'; import { Collapse, CollapsePanel, Button } from "tdesign-mobile-react";
import { toast } from '@/components/ui/toast'; import { toast } from "@/components/ui/toast";
import FriendSelection from '@/components/FriendSelection'; import FriendSelection from "@/components/FriendSelection";
import GroupSelection from '@/components/GroupSelection'; import GroupSelection from "@/components/GroupSelection";
import { get, post } from '@/api/request'; import { get, post } from "@/api/request";
// TODO: 引入微信好友/群组选择器、日期选择器等组件 // TODO: 引入微信好友/群组选择器、日期选择器等组件
interface WechatFriend { id: string; nickname: string; avatar: string; } interface WechatFriend {
interface WechatGroup { id: string; name: string; avatar: string; } id: string;
nickname: string;
avatar: string;
}
interface WechatGroup {
id: string;
name: string;
avatar: string;
}
interface ContentLibraryForm { interface ContentLibraryForm {
name: string; name: string;
sourceType: 'friends' | 'groups'; sourceType: "friends" | "groups";
keywordsInclude: string; keywordsInclude: string;
keywordsExclude: string; keywordsExclude: string;
startDate: string; startDate: string;
@@ -36,19 +44,21 @@ export default function NewContentLibraryPage() {
const { id } = useParams(); const { id } = useParams();
const isEdit = !!id; const isEdit = !!id;
const [form, setForm] = useState<ContentLibraryForm>({ const [form, setForm] = useState<ContentLibraryForm>({
name: '', name: "",
sourceType: 'friends', sourceType: "friends",
keywordsInclude: '', keywordsInclude: "",
keywordsExclude: '', keywordsExclude: "",
startDate: '', startDate: "",
endDate: '', endDate: "",
selectedFriends: [], selectedFriends: [],
selectedGroups: [], selectedGroups: [],
useAI: false, useAI: false,
aiPrompt: '', aiPrompt: "",
enabled: true, enabled: true,
}); });
const [selectedFriendObjs, setSelectedFriendObjs] = useState<WechatFriend[]>([]); const [selectedFriendObjs, setSelectedFriendObjs] = useState<WechatFriend[]>(
[]
);
const [selectedGroupObjs, setSelectedGroupObjs] = useState<WechatGroup[]>([]); const [selectedGroupObjs, setSelectedGroupObjs] = useState<WechatGroup[]>([]);
const [isFriendSelectorOpen, setIsFriendSelectorOpen] = useState(false); const [isFriendSelectorOpen, setIsFriendSelectorOpen] = useState(false);
const [isGroupSelectorOpen, setIsGroupSelectorOpen] = useState(false); const [isGroupSelectorOpen, setIsGroupSelectorOpen] = useState(false);
@@ -62,31 +72,66 @@ export default function NewContentLibraryPage() {
const data = res.data; const data = res.data;
// 时间戳转YYYY-MM-DD // 时间戳转YYYY-MM-DD
const formatDate = (val: number) => { const formatDate = (val: number) => {
if (!val || val === 0 || typeof val !== 'number' || isNaN(val) || val < 1000000000) return ''; if (
!val ||
val === 0 ||
typeof val !== "number" ||
isNaN(val) ||
val < 1000000000
)
return "";
try { try {
const d = new Date(val * 1000); const d = new Date(val * 1000);
if (isNaN(d.getTime())) return ''; if (isNaN(d.getTime())) return "";
return d.toISOString().slice(0, 10); return d.toISOString().slice(0, 10);
} catch { } catch {
return ''; return "";
} }
}; };
setForm(f => ({ setForm((f) => ({
...f, ...f,
name: data.name || '', name: data.name || "",
sourceType: data.sourceType === 1 ? 'friends' : 'groups', sourceType: data.sourceType === 1 ? "friends" : "groups",
keywordsInclude: (data.keywordInclude || []).join(','), keywordsInclude: (data.keywordInclude || []).join(","),
keywordsExclude: (data.keywordExclude || []).join(','), keywordsExclude: (data.keywordExclude || []).join(","),
startDate: formatDate(data.timeStart), startDate: formatDate(data.timeStart),
endDate: formatDate(data.timeEnd), endDate: formatDate(data.timeEnd),
selectedFriends: (data.selectedFriends || data.sourceFriends || []).map((fid: number | string) => ({ id: String(fid), nickname: String(fid), avatar: '' })), selectedFriends: (
selectedGroups: (data.sourceGroups || []).map((gid: number | string) => ({ id: String(gid), name: String(gid), avatar: '' })), 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, useAI: data.aiEnabled === 1,
aiPrompt: data.aiPrompt || '', aiPrompt: data.aiPrompt || "",
enabled: data.status === 1, enabled: data.status === 1,
})); }));
setSelectedFriendObjs((data.selectedFriends || data.sourceFriends || []).map((fid: number | string) => ({ id: String(fid), nickname: String(fid), avatar: '' }))); setSelectedFriendObjs(
setSelectedGroupObjs((data.sourceGroups || []).map((gid: number | string) => ({ id: String(gid), name: String(gid), avatar: '' }))); (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: "",
}))
);
} }
})(); })();
} }
@@ -100,27 +145,44 @@ export default function NewContentLibraryPage() {
const payload = { const payload = {
id: isEdit ? id : undefined, id: isEdit ? id : undefined,
name: form.name, name: form.name,
sourceType: form.sourceType === 'friends' ? 1 : 2, sourceType: form.sourceType === "friends" ? 1 : 2,
friends: form.selectedFriends.map(f => Number(f.id)), friends: form.selectedFriends.map((f) => Number(f.id)),
groups: form.selectedGroups.map(g => Number(g.id)), groups: form.selectedGroups.map((g) => Number(g.id)),
groupMembers: {}, groupMembers: {},
keywordInclude: form.keywordsInclude ? form.keywordsInclude.split(',').map(s => s.trim()).filter(Boolean) : [], keywordInclude: form.keywordsInclude
keywordExclude: form.keywordsExclude ? form.keywordsExclude.split(',').map(s => s.trim()).filter(Boolean) : [], ? form.keywordsInclude
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [],
keywordExclude: form.keywordsExclude
? form.keywordsExclude
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [],
aiPrompt: form.aiPrompt, aiPrompt: form.aiPrompt,
timeEnabled: form.startDate || form.endDate ? 1 : 0, timeEnabled: form.startDate || form.endDate ? 1 : 0,
startTime: form.startDate || '', startTime: form.startDate || "",
endTime: form.endDate || '', endTime: form.endDate || "",
status: form.enabled ? 1 : 0 status: form.enabled ? 1 : 0,
}; };
if (isEdit) { if (isEdit) {
await post('/v1/content/library/update', payload); await post("/v1/content/library/update", payload);
} else { } else {
await post('/v1/content/library/create', payload); await post("/v1/content/library/create", payload);
} }
toast({ title: isEdit ? '保存成功' : '创建成功', description: '内容库已保存' }); toast({
navigate('/content'); title: isEdit ? "保存成功" : "创建成功",
description: "内容库已保存",
});
navigate("/content");
} catch (error) { } catch (error) {
toast({ title: isEdit ? '保存失败' : '创建失败', description: '保存内容库失败', variant: 'destructive' }); toast({
title: isEdit ? "保存失败" : "创建失败",
description: "保存内容库失败",
variant: "destructive",
});
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -128,66 +190,114 @@ export default function NewContentLibraryPage() {
return ( return (
<Layout <Layout
header={<UnifiedHeader title={isEdit ? "编辑内容库" : "新建内容库"} showBack onBack={() => navigate(-1)} />} header={
<UnifiedHeader
title={isEdit ? "编辑内容库" : "新建内容库"}
showBack
onBack={() => navigate(-1)}
/>
}
footer={ footer={
<div className="p-4"> <div className="p-4">
<Button theme="primary" block onClick={handleSave} disabled={isSubmitting || !form.name} > <Button
{isSubmitting ? (isEdit ? '保存中...' : '创建中...') : (isEdit ? '保存' : '创建内容库')} theme="primary"
block
onClick={handleSave}
disabled={isSubmitting || !form.name}
>
{isSubmitting
? isEdit
? "保存中..."
: "创建中..."
: isEdit
? "保存"
: "创建内容库"}
</Button> </Button>
</div> </div>
} }
> >
<div className="flex-1 bg-gray-50 min-h-screen pb-16"> <div className="flex-1 bg-gray-50 ">
<div className="p-4 space-y-4 max-w-lg mx-auto"> <div className="p-4 space-y-4 max-w-lg mx-auto">
<Card className="p-4"> <Card className="p-4">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block font-medium mb-1"> <span className="text-red-500">*</span></label> <label className="block font-medium mb-1">
<span className="text-red-500">*</span>
</label>
<Input <Input
value={form.name} value={form.name}
onChange={e => setForm(f => ({ ...f, name: e.target.value }))} onChange={(e) =>
setForm((f) => ({ ...f, name: e.target.value }))
}
placeholder="请输入内容库名称" placeholder="请输入内容库名称"
required required
/> />
</div> </div>
<div> <div>
<label className="block font-medium mb-1"></label> <label className="block font-medium mb-1"></label>
<Tabs value={form.sourceType} onValueChange={val => setForm(f => ({ ...f, sourceType: val as 'friends' | 'groups' }))}> <Tabs
value={form.sourceType}
onValueChange={(val) =>
setForm((f) => ({
...f,
sourceType: val as "friends" | "groups",
}))
}
>
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="friends"></TabsTrigger> <TabsTrigger value="friends"></TabsTrigger>
<TabsTrigger value="groups"></TabsTrigger> <TabsTrigger value="groups"></TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="friends"> <TabsContent value="friends">
<FriendSelection <FriendSelection
selectedFriends={form.selectedFriends.map(f => f.id)} selectedFriends={form.selectedFriends.map((f) => f.id)}
onSelect={ids => setForm(f => ({ onSelect={(ids) =>
setForm((f) => ({
...f, ...f,
selectedFriends: ids.map(id => ({ id, nickname: id, avatar: '' })) selectedFriends: ids.map((id) => ({
}))} id,
nickname: id,
avatar: "",
})),
}))
}
onSelectDetail={setSelectedFriendObjs} onSelectDetail={setSelectedFriendObjs}
enableDeviceFilter={false} enableDeviceFilter={false}
placeholder="选择微信好友" placeholder="选择微信好友"
/> />
{selectedFriendObjs.length > 0 && ( {selectedFriendObjs.length > 0 && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{selectedFriendObjs.map(friend => ( {selectedFriendObjs.map((friend) => (
<div key={friend.id} className="flex items-center justify-between bg-gray-100 p-2 rounded-md"> <div
key={friend.id}
className="flex items-center justify-between bg-gray-100 p-2 rounded-md"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{friend.avatar ? ( {friend.avatar ? (
<img src={friend.avatar} alt={friend.nickname} className="w-8 h-8 rounded-full object-cover" /> <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> <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> <span>{friend.nickname}</span>
</div> </div>
<button <button
className="text-gray-400 hover:text-red-500 ml-2" className="text-gray-400 hover:text-red-500 ml-2"
onClick={() => { onClick={() => {
setForm(f => ({ setForm((f) => ({
...f, ...f,
selectedFriends: f.selectedFriends.filter(frd => frd.id !== friend.id) selectedFriends: f.selectedFriends.filter(
(frd) => frd.id !== friend.id
),
})); }));
setSelectedFriendObjs(objs => objs.filter(frd => frd.id !== friend.id)); setSelectedFriendObjs((objs) =>
objs.filter((frd) => frd.id !== friend.id)
);
}} }}
title="移除" title="移除"
> >
@@ -200,37 +310,54 @@ export default function NewContentLibraryPage() {
</TabsContent> </TabsContent>
<TabsContent value="groups"> <TabsContent value="groups">
<GroupSelection <GroupSelection
selectedGroups={form.selectedGroups.map(g => g.id)} selectedGroups={form.selectedGroups.map((g) => g.id)}
onSelect={ids => setForm(f => ({ onSelect={(ids) =>
setForm((f) => ({
...f, ...f,
selectedGroups: ids.map(id => { selectedGroups: ids.map((id) => {
const old = f.selectedGroups.find(g => g.id === id); const old = f.selectedGroups.find(
return old || { id, name: id, avatar: '' }; (g) => g.id === id
}) );
}))} return old || { id, name: id, avatar: "" };
}),
}))
}
onSelectDetail={setSelectedGroupObjs} onSelectDetail={setSelectedGroupObjs}
placeholder="选择群聊" placeholder="选择群聊"
/> />
{selectedGroupObjs.length > 0 && ( {selectedGroupObjs.length > 0 && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{selectedGroupObjs.map(group => ( {selectedGroupObjs.map((group) => (
<div key={group.id} className="flex items-center justify-between bg-gray-100 p-2 rounded-md"> <div
key={group.id}
className="flex items-center justify-between bg-gray-100 p-2 rounded-md"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{group.avatar ? ( {group.avatar ? (
<img src={group.avatar} alt={group.name} className="w-8 h-8 rounded-full object-cover" /> <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> <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> <span>{group.name}</span>
</div> </div>
<button <button
className="text-gray-400 hover:text-red-500 ml-2" className="text-gray-400 hover:text-red-500 ml-2"
onClick={() => { onClick={() => {
setForm(f => ({ setForm((f) => ({
...f, ...f,
selectedGroups: f.selectedGroups.filter(grp => grp.id !== group.id) selectedGroups: f.selectedGroups.filter(
(grp) => grp.id !== group.id
),
})); }));
setSelectedGroupObjs(objs => objs.filter(grp => grp.id !== group.id)); setSelectedGroupObjs((objs) =>
objs.filter((grp) => grp.id !== group.id)
);
}} }}
title="移除" title="移除"
> >
@@ -247,18 +374,32 @@ export default function NewContentLibraryPage() {
<CollapsePanel header="关键字设置" value="keywords"> <CollapsePanel header="关键字设置" value="keywords">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block font-medium mb-1"></label> <label className="block font-medium mb-1">
</label>
<Textarea <Textarea
value={form.keywordsInclude} value={form.keywordsInclude}
onChange={e => setForm(f => ({ ...f, keywordsInclude: e.target.value }))} onChange={(e) =>
setForm((f) => ({
...f,
keywordsInclude: e.target.value,
}))
}
placeholder="如果设置了关键字,系统只会采集含有关键字的内容。多个关键字,用半角的','隔开。" placeholder="如果设置了关键字,系统只会采集含有关键字的内容。多个关键字,用半角的','隔开。"
/> />
</div> </div>
<div> <div>
<label className="block font-medium mb-1"></label> <label className="block font-medium mb-1">
</label>
<Textarea <Textarea
value={form.keywordsExclude} value={form.keywordsExclude}
onChange={e => setForm(f => ({ ...f, keywordsExclude: e.target.value }))} onChange={(e) =>
setForm((f) => ({
...f,
keywordsExclude: e.target.value,
}))
}
placeholder="排除含有这些关键字的内容。多个关键字,用半角的','隔开。" placeholder="排除含有这些关键字的内容。多个关键字,用半角的','隔开。"
/> />
</div> </div>
@@ -269,17 +410,26 @@ export default function NewContentLibraryPage() {
<div> <div>
<label className="block font-medium">AI</label> <label className="block font-medium">AI</label>
</div> </div>
<div className='w-10'> <div className="w-10">
<Switch checked={form.useAI} onCheckedChange={checked => setForm(f => ({ ...f, useAI: checked }))} /> <Switch
checked={form.useAI}
onCheckedChange={(checked) =>
setForm((f) => ({ ...f, useAI: checked }))
}
/>
</div> </div>
</div> </div>
<p className="text-sm text-gray-500 mt-1 ">AI之后AI重新生成内容</p> <p className="text-sm text-gray-500 mt-1 ">
AI之后AI重新生成内容
</p>
{form.useAI && ( {form.useAI && (
<div> <div>
<label className="block font-medium mb-1">AI </label> <label className="block font-medium mb-1">AI </label>
<Textarea <Textarea
value={form.aiPrompt} value={form.aiPrompt}
onChange={e => setForm(f => ({ ...f, aiPrompt: e.target.value }))} onChange={(e) =>
setForm((f) => ({ ...f, aiPrompt: e.target.value }))
}
placeholder="请输入 AI 提示词" placeholder="请输入 AI 提示词"
/> />
</div> </div>
@@ -287,31 +437,41 @@ export default function NewContentLibraryPage() {
<div> <div>
<label className="block font-medium mb-2"></label> <label className="block font-medium mb-2"></label>
{/* TODO: 替换为TDesign日期范围选择器 */} {/* TODO: 替换为TDesign日期范围选择器 */}
<div className='flex mb-2' style={{ justifyContent: 'space-between' }}> <div
<label className='text-sm w-20 '></label> className="flex mb-2"
style={{ justifyContent: "space-between" }}
>
<label className="text-sm w-20 "></label>
<Input <Input
type="date" type="date"
value={form.startDate} value={form.startDate}
onChange={e => setForm(f => ({ ...f, startDate: e.target.value }))} onChange={(e) =>
setForm((f) => ({ ...f, startDate: e.target.value }))
}
className="inline-block w-1/2 " className="inline-block w-1/2 "
/> />
</div> </div>
<div className='flex '> <div className="flex ">
<label className='text-sm w-20' ></label> <label className="text-sm w-20"></label>
<Input <Input
type="date" type="date"
value={form.endDate} value={form.endDate}
onChange={e => setForm(f => ({ ...f, endDate: e.target.value }))} onChange={(e) =>
setForm((f) => ({ ...f, endDate: e.target.value }))
}
className="inline-block w-1/2" className="inline-block w-1/2"
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="block font-medium mb-1"></label> <label className="block font-medium mb-1"></label>
<Switch checked={form.enabled} onCheckedChange={checked => setForm(f => ({ ...f, enabled: checked }))} /> <Switch
checked={form.enabled}
onCheckedChange={(checked) =>
setForm((f) => ({ ...f, enabled: checked }))
}
/>
</div> </div>
</div> </div>
</Card> </Card>
</div> </div>

View File

@@ -215,11 +215,7 @@ export default function NewPlan() {
<header className="sticky top-0 z-10 bg-white border-b"> <header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center justify-between h-14 px-4"> <div className="flex items-center justify-between h-14 px-4">
<div className="flex items-center"> <div className="flex items-center">
<Button <Button variant="ghost" size="icon" onClick={() => router(-1)}>
variant="ghost"
size="icon"
onClick={() => router("/scenarios")}
>
<ChevronLeft className="h-5 w-5" /> <ChevronLeft className="h-5 w-5" />
</Button> </Button>
</div> </div>