选择的处理下

This commit is contained in:
笔记本里的永平
2025-07-17 09:59:54 +08:00
parent 42ba2590f7
commit 43e873f83a
3 changed files with 341 additions and 166 deletions

View File

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

View File

@@ -42,62 +42,74 @@ export function DeviceSelectionDialog({
const [loading, setLoading] = useState(false);
const [devices, setDevices] = useState<Device[]>([]);
// 获取设备列表
const fetchDevices = useCallback(async () => {
setLoading(true);
try {
const response = await fetchDeviceList(1, 100, searchQuery);
if (response.code === 200 && response.data) {
// 转换服务端数据格式为组件需要的格式
const convertedDevices: Device[] = response.data.list.map(
(serverDevice: ServerDevice) => ({
id: serverDevice.id.toString(),
name: serverDevice.memo || `设备 ${serverDevice.id}`,
imei: serverDevice.imei,
wxid: serverDevice.wechatId || "",
status: serverDevice.alive === 1 ? "online" : "offline",
usedInPlans: 0, // 这个字段需要从其他API获取
nickname: serverDevice.nickname || "",
})
// 获取设备列表支持keyword
const fetchDevices = useCallback(
async (keyword: string = "") => {
setLoading(true);
try {
const response = await fetchDeviceList(
1,
100,
keyword.trim() || undefined
);
setDevices(convertedDevices);
} else {
if (response.code === 200 && response.data) {
// 转换服务端数据格式为组件需要的格式
const convertedDevices: Device[] = response.data.list.map(
(serverDevice: ServerDevice) => ({
id: serverDevice.id.toString(),
name: serverDevice.memo || `设备 ${serverDevice.id}`,
imei: serverDevice.imei,
wxid: serverDevice.wechatId || "",
status: serverDevice.alive === 1 ? "online" : "offline",
usedInPlans: 0, // 这个字段需要从其他API获取
nickname: serverDevice.nickname || "",
})
);
setDevices(convertedDevices);
} else {
toast({
title: "获取设备列表失败",
description: response.msg,
variant: "destructive",
});
}
} catch (error) {
console.error("获取设备列表失败:", error);
toast({
title: "获取设备列表失败",
description: response.msg,
description: "请检查网络连接",
variant: "destructive",
});
} finally {
setLoading(false);
}
} catch (error) {
console.error("获取设备列表失败:", error);
toast({
title: "获取设备列表失败",
description: "请检查网络连接",
variant: "destructive",
});
} finally {
setLoading(false);
}
}, [searchQuery, toast]);
},
[toast]
);
// 打开弹窗时获取设备列表
useEffect(() => {
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 matchesSearch =
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.wxid.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus =
statusFilter === "all" ||
(statusFilter === "online" && device.status === "online") ||
(statusFilter === "offline" && device.status === "offline");
return matchesSearch && matchesStatus;
return matchesStatus;
});
const handleDeviceSelect = (deviceId: string) => {
@@ -137,7 +149,7 @@ export function DeviceSelectionDialog({
<Button
variant="outline"
size="icon"
onClick={fetchDevices}
onClick={() => fetchDevices(searchQuery)}
disabled={loading}
>
{loading ? (

View File

@@ -1,25 +1,33 @@
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';
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 WechatFriend {
id: string;
nickname: string;
avatar: string;
}
interface WechatGroup {
id: string;
name: string;
avatar: string;
}
interface ContentLibraryForm {
name: string;
sourceType: 'friends' | 'groups';
sourceType: "friends" | "groups";
keywordsInclude: string;
keywordsExclude: string;
startDate: string;
@@ -36,19 +44,21 @@ export default function NewContentLibraryPage() {
const { id } = useParams();
const isEdit = !!id;
const [form, setForm] = useState<ContentLibraryForm>({
name: '',
sourceType: 'friends',
keywordsInclude: '',
keywordsExclude: '',
startDate: '',
endDate: '',
name: "",
sourceType: "friends",
keywordsInclude: "",
keywordsExclude: "",
startDate: "",
endDate: "",
selectedFriends: [],
selectedGroups: [],
useAI: false,
aiPrompt: '',
aiPrompt: "",
enabled: true,
});
const [selectedFriendObjs, setSelectedFriendObjs] = useState<WechatFriend[]>([]);
const [selectedFriendObjs, setSelectedFriendObjs] = useState<WechatFriend[]>(
[]
);
const [selectedGroupObjs, setSelectedGroupObjs] = useState<WechatGroup[]>([]);
const [isFriendSelectorOpen, setIsFriendSelectorOpen] = useState(false);
const [isGroupSelectorOpen, setIsGroupSelectorOpen] = useState(false);
@@ -62,31 +72,66 @@ export default function NewContentLibraryPage() {
const data = res.data;
// 时间戳转YYYY-MM-DD
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 {
const d = new Date(val * 1000);
if (isNaN(d.getTime())) return '';
if (isNaN(d.getTime())) return "";
return d.toISOString().slice(0, 10);
} catch {
return '';
return "";
}
};
setForm(f => ({
setForm((f) => ({
...f,
name: data.name || '',
sourceType: data.sourceType === 1 ? 'friends' : 'groups',
keywordsInclude: (data.keywordInclude || []).join(','),
keywordsExclude: (data.keywordExclude || []).join(','),
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: '' })),
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 || '',
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: '' })));
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: "",
}))
);
}
})();
}
@@ -100,27 +145,44 @@ export default function NewContentLibraryPage() {
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)),
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) : [],
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
startTime: form.startDate || "",
endTime: form.endDate || "",
status: form.enabled ? 1 : 0,
};
if (isEdit) {
await post('/v1/content/library/update', payload);
await post("/v1/content/library/update", payload);
} else {
await post('/v1/content/library/create', payload);
await post("/v1/content/library/create", payload);
}
toast({ title: isEdit ? '保存成功' : '创建成功', description: '内容库已保存' });
navigate('/content');
toast({
title: isEdit ? "保存成功" : "创建成功",
description: "内容库已保存",
});
navigate("/content");
} catch (error) {
toast({ title: isEdit ? '保存失败' : '创建失败', description: '保存内容库失败', variant: 'destructive' });
toast({
title: isEdit ? "保存失败" : "创建失败",
description: "保存内容库失败",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
@@ -128,66 +190,114 @@ export default function NewContentLibraryPage() {
return (
<Layout
header={<UnifiedHeader title={isEdit ? "编辑内容库" : "新建内容库"} 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 ? (isEdit ? '保存中...' : '创建中...') : (isEdit ? '保存' : '创建内容库')}
</Button>
</div>
<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 min-h-screen pb-16">
<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>
<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 }))}
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' }))}>
<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: '' }))
}))}
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">
{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" />
<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>
</div>
<button
className="text-gray-400 hover:text-red-500 ml-2"
onClick={() => {
setForm(f => ({
setForm((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="移除"
>
@@ -200,37 +310,54 @@ export default function NewContentLibraryPage() {
</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: '' };
})
}))}
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">
{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" />
<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>
</div>
<button
className="text-gray-400 hover:text-red-500 ml-2"
onClick={() => {
setForm(f => ({
setForm((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="移除"
>
@@ -247,18 +374,32 @@ export default function NewContentLibraryPage() {
<CollapsePanel header="关键字设置" value="keywords">
<div className="space-y-4">
<div>
<label className="block font-medium mb-1"></label>
<label className="block font-medium mb-1">
</label>
<Textarea
value={form.keywordsInclude}
onChange={e => setForm(f => ({ ...f, keywordsInclude: e.target.value }))}
onChange={(e) =>
setForm((f) => ({
...f,
keywordsInclude: e.target.value,
}))
}
placeholder="如果设置了关键字,系统只会采集含有关键字的内容。多个关键字,用半角的','隔开。"
/>
</div>
<div>
<label className="block font-medium mb-1"></label>
<label className="block font-medium mb-1">
</label>
<Textarea
value={form.keywordsExclude}
onChange={e => setForm(f => ({ ...f, keywordsExclude: e.target.value }))}
onChange={(e) =>
setForm((f) => ({
...f,
keywordsExclude: e.target.value,
}))
}
placeholder="排除含有这些关键字的内容。多个关键字,用半角的','隔开。"
/>
</div>
@@ -269,17 +410,26 @@ export default function NewContentLibraryPage() {
<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 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>
<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 }))}
onChange={(e) =>
setForm((f) => ({ ...f, aiPrompt: e.target.value }))
}
placeholder="请输入 AI 提示词"
/>
</div>
@@ -287,31 +437,41 @@ export default function NewContentLibraryPage() {
<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>
<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 }))}
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>
<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 }))}
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 }))} />
<Switch
checked={form.enabled}
onCheckedChange={(checked) =>
setForm((f) => ({ ...f, enabled: checked }))
}
/>
</div>
</div>
</Card>
</div>
@@ -319,4 +479,4 @@ export default function NewContentLibraryPage() {
</div>
</Layout>
);
}
}