内容库可以选择设备
This commit is contained in:
@@ -26,4 +26,5 @@ export interface DeviceSelectionProps {
|
||||
showSelectedList?: boolean; // 新增
|
||||
readonly?: boolean; // 新增
|
||||
deviceGroups?: any[]; // 传递设备组数据
|
||||
singleSelect?: boolean; // 新增,是否单选模式
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
||||
showInput = true,
|
||||
showSelectedList = true,
|
||||
readonly = false,
|
||||
singleSelect = false,
|
||||
}) => {
|
||||
// 弹窗控制
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
@@ -37,6 +38,9 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
||||
// 获取显示文本
|
||||
const getDisplayText = () => {
|
||||
if (selectedOptions.length === 0) return "";
|
||||
if (singleSelect && selectedOptions.length > 0) {
|
||||
return selectedOptions[0].memo || selectedOptions[0].wechatId || "已选择设备";
|
||||
}
|
||||
return `已选择 ${selectedOptions.length} 个设备`;
|
||||
};
|
||||
|
||||
@@ -179,6 +183,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
||||
onClose={() => setRealVisible(false)}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelect={onSelect}
|
||||
singleSelect={singleSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ interface SelectionPopupProps {
|
||||
onClose: () => void;
|
||||
selectedOptions: DeviceSelectionItem[];
|
||||
onSelect: (devices: DeviceSelectionItem[]) => void;
|
||||
singleSelect?: boolean;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -21,6 +22,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
onClose,
|
||||
selectedOptions,
|
||||
onSelect,
|
||||
singleSelect = false,
|
||||
}) => {
|
||||
// 设备数据
|
||||
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
|
||||
@@ -110,13 +112,23 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
|
||||
// 处理设备选择
|
||||
const handleDeviceToggle = (device: DeviceSelectionItem) => {
|
||||
if (tempSelectedOptions.some(v => v.id === device.id)) {
|
||||
setTempSelectedOptions(
|
||||
tempSelectedOptions.filter(v => v.id !== device.id),
|
||||
);
|
||||
if (singleSelect) {
|
||||
// 单选模式:如果已选中,则取消选择;否则替换为当前设备
|
||||
if (tempSelectedOptions.some(v => v.id === device.id)) {
|
||||
setTempSelectedOptions([]);
|
||||
} else {
|
||||
setTempSelectedOptions([device]);
|
||||
}
|
||||
} else {
|
||||
const newSelectedOptions = [...tempSelectedOptions, device];
|
||||
setTempSelectedOptions(newSelectedOptions);
|
||||
// 多选模式:原有的逻辑
|
||||
if (tempSelectedOptions.some(v => v.id === device.id)) {
|
||||
setTempSelectedOptions(
|
||||
tempSelectedOptions.filter(v => v.id !== device.id),
|
||||
);
|
||||
} else {
|
||||
const newSelectedOptions = [...tempSelectedOptions, device];
|
||||
setTempSelectedOptions(newSelectedOptions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,6 +191,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
totalPages={totalPages}
|
||||
loading={loading}
|
||||
selectedCount={tempSelectedOptions.length}
|
||||
singleSelect={singleSelect}
|
||||
onPageChange={setCurrentPage}
|
||||
onCancel={onClose}
|
||||
onConfirm={() => {
|
||||
@@ -187,7 +200,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
onClose();
|
||||
}}
|
||||
isAllSelected={isCurrentPageAllSelected}
|
||||
onSelectAll={handleSelectAllCurrentPage}
|
||||
onSelectAll={singleSelect ? undefined : handleSelectAllCurrentPage}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -95,9 +95,9 @@ export default function FriendSelection({
|
||||
{(selectedOptions || []).map(friend => (
|
||||
<div key={friend.id} className={style.selectedListRow}>
|
||||
<div className={style.selectedListRowContent}>
|
||||
<Avatar src={friend.friendAvatar} />
|
||||
<Avatar src={friend.avatar || friend.friendAvatar} />
|
||||
<div className={style.selectedListRowContentText}>
|
||||
<div>{friend.friendName}</div>
|
||||
<div>{friend.nickname || friend.friendName}</div>
|
||||
<div>{friend.wechatId}</div>
|
||||
</div>
|
||||
{!readonly && (
|
||||
|
||||
@@ -14,6 +14,7 @@ interface PopupFooterProps {
|
||||
// 全选功能相关
|
||||
isAllSelected?: boolean;
|
||||
onSelectAll?: (checked: boolean) => void;
|
||||
singleSelect?: boolean;
|
||||
}
|
||||
|
||||
const PopupFooter: React.FC<PopupFooterProps> = ({
|
||||
@@ -26,20 +27,23 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
|
||||
onConfirm,
|
||||
isAllSelected = false,
|
||||
onSelectAll,
|
||||
singleSelect = false,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* 分页栏 */}
|
||||
<div className={style.paginationRow}>
|
||||
<div className={style.totalCount}>
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onChange={e => onSelectAll(e.target.checked)}
|
||||
className={style.selectAllCheckbox}
|
||||
>
|
||||
全选当前页
|
||||
</Checkbox>
|
||||
</div>
|
||||
{onSelectAll && (
|
||||
<div className={style.totalCount}>
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onChange={e => onSelectAll(e.target.checked)}
|
||||
className={style.selectAllCheckbox}
|
||||
>
|
||||
全选当前页
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.paginationControls}>
|
||||
<Button
|
||||
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
||||
@@ -61,7 +65,13 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.popupFooter}>
|
||||
<div className={style.selectedCount}>已选择 {selectedCount} 条记录</div>
|
||||
<div className={style.selectedCount}>
|
||||
{singleSelect
|
||||
? selectedCount > 0
|
||||
? "已选择设备"
|
||||
: "未选择设备"
|
||||
: `已选择 ${selectedCount} 条记录`}
|
||||
</div>
|
||||
<div className={style.footerBtnGroup}>
|
||||
<Button color="primary" variant="filled" onClick={onCancel}>
|
||||
取消
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
.form-page {
|
||||
background: #f7f8fa;
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.form-main {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 18px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
||||
padding: 24px 18px 18px 18px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.source-tag {
|
||||
font-size: 14px;
|
||||
padding: 4px 16px;
|
||||
border-radius: 16px;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@@ -32,9 +46,23 @@
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin-top: 28px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
padding-left: 8px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: #1890ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-block {
|
||||
@@ -47,40 +75,78 @@
|
||||
.adm-tabs-header {
|
||||
background: #f7f8fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
.adm-tabs-tab {
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 0;
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.adm-tabs-tab-active {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.adm-tabs-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
margin-top: 12px;
|
||||
.keyword-collapse {
|
||||
.adm-collapse-panel-header {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.adm-collapse-panel-content {
|
||||
padding-bottom: 8px;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 18px 14px 10px 14px;
|
||||
margin-top: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
padding: 16px 0 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 22px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.adm-input {
|
||||
min-height: 42px;
|
||||
font-size: 15px;
|
||||
border-radius: 7px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.keyword-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.keyword-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.keyword-arrow {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.keyword-collapse .adm-collapse-panel-active .keyword-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.ai-row,
|
||||
@@ -91,9 +157,53 @@
|
||||
}
|
||||
|
||||
.ai-desc {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.content-type-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.content-type-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.content-type-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.content-type-btn {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
outline: none;
|
||||
|
||||
&:not(.active) {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.date-row,
|
||||
@@ -109,6 +219,179 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.time-limit-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.time-limit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.date-inputs {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.date-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 100%;
|
||||
|
||||
.ant-input {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #222;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enable-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.enable-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.device-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.device-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.online {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.device-wechat-id {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.device-arrow {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.device-input-wrapper {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.device-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.keyword-select {
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selector {
|
||||
min-height: 44px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 32px;
|
||||
height: 48px !important;
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Input as AntdInput, Switch } from "antd";
|
||||
import { Input as AntdInput, Switch, Tag } from "antd";
|
||||
import { Button, Collapse, Toast, DatePicker, Tabs } from "antd-mobile";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import GroupSelection from "@/components/GroupSelection";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
import request from "@/api/request";
|
||||
import { getContentLibraryDetail, updateContentLibrary } from "./api";
|
||||
import { GroupSelectionItem } from "@/components/GroupSelection/data";
|
||||
import { FriendSelectionItem } from "@/components/FriendSelection/data";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
|
||||
const { TextArea } = AntdInput;
|
||||
|
||||
@@ -29,6 +32,15 @@ export default function ContentForm() {
|
||||
const isEdit = !!id;
|
||||
const [sourceType, setSourceType] = useState<"friends" | "groups">("friends");
|
||||
const [name, setName] = useState("");
|
||||
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>([]);
|
||||
const [deviceSelectionVisible, setDeviceSelectionVisible] = useState(false);
|
||||
|
||||
// 处理设备选择(单选模式)
|
||||
const handleDeviceSelect = (devices: DeviceSelectionItem[]) => {
|
||||
// 单选模式:只保留最后一个选中的设备
|
||||
setSelectedDevices(devices.length > 0 ? [devices[devices.length - 1]] : []);
|
||||
setDeviceSelectionVisible(false);
|
||||
};
|
||||
const [friendsGroups, setSelectedFriends] = useState<string[]>([]);
|
||||
const [friendsGroupsOptions, setSelectedFriendsOptions] = useState<
|
||||
FriendSelectionItem[]
|
||||
@@ -64,6 +76,23 @@ export default function ContentForm() {
|
||||
.then(data => {
|
||||
setName(data.name || "");
|
||||
setSourceType(data.sourceType === 1 ? "friends" : "groups");
|
||||
// 详情接口中的采集设备数据可能在 devices / selectedDevices / deviceGroupsOptions 中
|
||||
const deviceOptions: DeviceSelectionItem[] =
|
||||
data.devices ||
|
||||
data.selectedDevices ||
|
||||
(data.deviceGroupsOptions
|
||||
? (data.deviceGroupsOptions as any[]).map(item => ({
|
||||
id: item.id,
|
||||
memo: item.memo,
|
||||
imei: item.imei,
|
||||
wechatId: item.wechatId,
|
||||
status: item.alive === 1 ? "online" : "offline",
|
||||
nickname: item.nickname,
|
||||
avatar: item.avatar,
|
||||
totalFriend: item.totalFriend,
|
||||
}))
|
||||
: []);
|
||||
setSelectedDevices(deviceOptions || []);
|
||||
setSelectedFriends(data.sourceFriends || []);
|
||||
setSelectedGroups(data.selectedGroups || []);
|
||||
setSelectedGroupsOptions(data.selectedGroupsOptions || []);
|
||||
@@ -72,7 +101,13 @@ export default function ContentForm() {
|
||||
setKeywordsExclude((data.keywordExclude || []).join(","));
|
||||
setCatchType(data.catchType || ["text", "image", "video"]);
|
||||
setAIPrompt(data.aiPrompt || "");
|
||||
setUseAI(!!data.aiPrompt);
|
||||
// aiEnabled 为 AI 提示词开关,1 开启 0 关闭
|
||||
if (typeof data.aiEnabled !== "undefined") {
|
||||
setUseAI(data.aiEnabled === 1);
|
||||
} else {
|
||||
// 兼容旧数据,默认根据是否有 aiPrompt 判断
|
||||
setUseAI(!!data.aiPrompt);
|
||||
}
|
||||
setEnabled(data.status === 1);
|
||||
// 时间范围
|
||||
const start = data.timeStart || data.startTime;
|
||||
@@ -103,6 +138,7 @@ export default function ContentForm() {
|
||||
const payload = {
|
||||
name,
|
||||
sourceType: sourceType === "friends" ? 1 : 2,
|
||||
devices: selectedDevices.map(d => d.id),
|
||||
friendsGroups: friendsGroups,
|
||||
wechatGroups: selectedGroups,
|
||||
groupMembers: {},
|
||||
@@ -115,7 +151,8 @@ export default function ContentForm() {
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
catchType,
|
||||
aiPrompt,
|
||||
aiPrompt,
|
||||
aiEnabled: useAI ? 1 : 0,
|
||||
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
|
||||
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
|
||||
endTime: dateRange[1] ? formatDate(dateRange[1]) : "",
|
||||
@@ -178,7 +215,7 @@ export default function ContentForm() {
|
||||
onSubmit={e => e.preventDefault()}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<div className={style["form-card"]}>
|
||||
<label className={style["form-label"]}>
|
||||
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
|
||||
内容库名称
|
||||
@@ -191,8 +228,100 @@ export default function ContentForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style["section-title"]}>数据来源配置</div>
|
||||
<div className={style["form-section"]}>
|
||||
<div className={style["form-card"]}>
|
||||
<label className={style["form-label"]}>
|
||||
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
|
||||
来源渠道
|
||||
</label>
|
||||
<Tag color="blue" className={style["source-tag"]}>
|
||||
微信朋友圈
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<div className={style["form-card"]}>
|
||||
<label className={style["form-label"]}>
|
||||
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
|
||||
选择采集设备
|
||||
</label>
|
||||
{selectedDevices.length > 0 ? (
|
||||
<div
|
||||
className={style["device-card"]}
|
||||
onClick={() => setDeviceSelectionVisible(true)}
|
||||
>
|
||||
<div className={style["device-avatar"]}>
|
||||
{selectedDevices[0].avatar ? (
|
||||
<img
|
||||
src={selectedDevices[0].avatar}
|
||||
alt="头像"
|
||||
className={style["avatar-img"]}
|
||||
/>
|
||||
) : (
|
||||
<span className={style["avatar-text"]}>
|
||||
{(selectedDevices[0].memo ||
|
||||
selectedDevices[0].wechatId ||
|
||||
"设")[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["device-info"]}>
|
||||
<div className={style["device-name-row"]}>
|
||||
<span className={style["device-name"]}>
|
||||
{selectedDevices[0].nickname || selectedDevices[0].memo}
|
||||
</span>
|
||||
{selectedDevices[0].memo &&
|
||||
selectedDevices[0].nickname &&
|
||||
selectedDevices[0].memo !== selectedDevices[0].nickname && (
|
||||
<span className={style["device-tag"]}>
|
||||
{selectedDevices[0].memo}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`${style["status-dot"]} ${
|
||||
selectedDevices[0].status === "online"
|
||||
? style["online"]
|
||||
: style["offline"]
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["device-wechat-id"]}>
|
||||
微信ID: {selectedDevices[0].wechatId}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["device-arrow"]}>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style["device-input-wrapper"]}>
|
||||
<DeviceSelection
|
||||
selectedOptions={selectedDevices}
|
||||
onSelect={handleDeviceSelect}
|
||||
placeholder="选择采集设备"
|
||||
showInput={true}
|
||||
showSelectedList={false}
|
||||
singleSelect={true}
|
||||
className={style["device-input"]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 隐藏的设备选择组件,用于打开弹窗 */}
|
||||
<div style={{ display: "none" }}>
|
||||
<DeviceSelection
|
||||
selectedOptions={selectedDevices}
|
||||
onSelect={handleDeviceSelect}
|
||||
placeholder="选择采集设备"
|
||||
showInput={false}
|
||||
showSelectedList={false}
|
||||
singleSelect={true}
|
||||
mode="dialog"
|
||||
open={deviceSelectionVisible}
|
||||
onOpenChange={setDeviceSelectionVisible}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style["section-title"]}>采集内容配置</div>
|
||||
<div className={style["form-card"]}>
|
||||
<Tabs
|
||||
activeKey={sourceType}
|
||||
onChange={key => setSourceType(key as "friends" | "groups")}
|
||||
@@ -203,6 +332,7 @@ export default function ContentForm() {
|
||||
selectedOptions={friendsGroupsOptions}
|
||||
onSelect={handleFriendsChange}
|
||||
placeholder="选择微信好友"
|
||||
deviceIds={selectedDevices.map(d => Number(d.id))}
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="选择聊天群" key="groups">
|
||||
@@ -215,54 +345,53 @@ export default function ContentForm() {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Collapse className={style["collapse"]}>
|
||||
<Collapse.Panel
|
||||
key="keywords"
|
||||
title={<span className={style["form-label"]}>关键词设置</span>}
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>包含关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsInclude}
|
||||
onChange={e => setKeywordsInclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>排除关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsExclude}
|
||||
onChange={e => setKeywordsExclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<div className={style["form-card"]}>
|
||||
<Collapse className={style["keyword-collapse"]}>
|
||||
<Collapse.Panel
|
||||
key="keywords"
|
||||
title={
|
||||
<div className={style["keyword-header"]}>
|
||||
<span className={style["keyword-title"]}>关键词设置</span>
|
||||
<DownOutlined className={style["keyword-arrow"]} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>包含关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsInclude}
|
||||
onChange={e => setKeywordsInclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>排除关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsExclude}
|
||||
onChange={e => setKeywordsExclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
{/* 采集内容类型 */}
|
||||
<div className={style["section-title"]}>采集内容类型</div>
|
||||
<div className={style["form-section"]}>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
|
||||
<div className={style["form-card"]}>
|
||||
<div className={style["content-type-header"]}>
|
||||
<span className={style["content-type-title"]}>采集内容类型</span>
|
||||
</div>
|
||||
<div className={style["content-type-buttons"]}>
|
||||
{["text", "image", "video"].map(type => (
|
||||
<div
|
||||
<button
|
||||
key={type}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "8px 12px",
|
||||
border: "1px solid #d9d9d9",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: catchType.includes(type)
|
||||
? "#1890ff"
|
||||
: "#fff",
|
||||
color: catchType.includes(type) ? "#fff" : "#333",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className={`${style["content-type-btn"]} ${
|
||||
catchType.includes(type) ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCatchType(prev =>
|
||||
prev.includes(type)
|
||||
@@ -271,100 +400,101 @@ export default function ContentForm() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{type === "text"
|
||||
? "文本"
|
||||
: type === "image"
|
||||
? "图片"
|
||||
: "视频"}
|
||||
</span>
|
||||
</div>
|
||||
{type === "text"
|
||||
? "文本"
|
||||
: type === "image"
|
||||
? "图片"
|
||||
: "视频"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style["section-title"]}>是否启用AI</div>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Switch checked={useAI} onChange={setUseAI} />
|
||||
<span className={style["ai-desc"]}>
|
||||
启用AI后,该内容库下的所有内容都会通过AI生成
|
||||
</span>
|
||||
</div>
|
||||
{useAI && (
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>AI提示词</label>
|
||||
<TextArea
|
||||
<div className={style["form-card"]}>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Switch checked={useAI} onChange={setUseAI} />
|
||||
<span className={style["ai-desc"]}>
|
||||
启用后,该内容库下的内容会通过AI生成
|
||||
</span>
|
||||
</div>
|
||||
{useAI && (
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>AI提示词</label>
|
||||
<TextArea
|
||||
placeholder="请输入AI提示词"
|
||||
value={aiPrompt}
|
||||
onChange={e => setAIPrompt(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 4, maxRows: 10 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={style["section-title"]}>时间限制</div>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", gap: 12 }}
|
||||
>
|
||||
<label>开始时间</label>
|
||||
<div style={{ flex: 1 }}>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={dateRange[0] ? dateRange[0].toLocaleDateString() : ""}
|
||||
placeholder="年/月/日"
|
||||
className={style["input"]}
|
||||
onClick={() => setShowStartPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showStartPicker}
|
||||
title="开始时间"
|
||||
value={dateRange[0]}
|
||||
onClose={() => setShowStartPicker(false)}
|
||||
onConfirm={val => {
|
||||
setDateRange([val, dateRange[1]]);
|
||||
setShowStartPicker(false);
|
||||
}}
|
||||
/>
|
||||
<div className={style["form-card"]}>
|
||||
<div className={style["time-limit-header"]}>
|
||||
<span className={style["time-limit-title"]}>时间限制</span>
|
||||
</div>
|
||||
<label>结束时间</label>
|
||||
<div style={{ flex: 1 }}>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={dateRange[1] ? dateRange[1].toLocaleDateString() : ""}
|
||||
placeholder="年/月/日"
|
||||
className={style["input"]}
|
||||
onClick={() => setShowEndPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showEndPicker}
|
||||
title="结束时间"
|
||||
value={dateRange[1]}
|
||||
onClose={() => setShowEndPicker(false)}
|
||||
onConfirm={val => {
|
||||
setDateRange([dateRange[0], val]);
|
||||
setShowEndPicker(false);
|
||||
}}
|
||||
/>
|
||||
<div className={style["date-inputs"]}>
|
||||
<div className={style["date-item"]}>
|
||||
<label className={style["date-label"]}>开始时间</label>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={
|
||||
dateRange[0]
|
||||
? `${dateRange[0].getFullYear()}/${String(dateRange[0].getMonth() + 1).padStart(2, "0")}/${String(dateRange[0].getDate()).padStart(2, "0")}`
|
||||
: ""
|
||||
}
|
||||
placeholder="年/月/日"
|
||||
className={style["date-input"]}
|
||||
onClick={() => setShowStartPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showStartPicker}
|
||||
title="开始时间"
|
||||
value={dateRange[0]}
|
||||
onClose={() => setShowStartPicker(false)}
|
||||
onConfirm={val => {
|
||||
setDateRange([val, dateRange[1]]);
|
||||
setShowStartPicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["date-item"]}>
|
||||
<label className={style["date-label"]}>结束时间</label>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={
|
||||
dateRange[1]
|
||||
? `${dateRange[1].getFullYear()}/${String(dateRange[1].getMonth() + 1).padStart(2, "0")}/${String(dateRange[1].getDate()).padStart(2, "0")}`
|
||||
: ""
|
||||
}
|
||||
placeholder="年/月/日"
|
||||
className={style["date-input"]}
|
||||
onClick={() => setShowEndPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showEndPicker}
|
||||
title="结束时间"
|
||||
value={dateRange[1]}
|
||||
onClose={() => setShowEndPicker(false)}
|
||||
onConfirm={val => {
|
||||
setDateRange([dateRange[0], val]);
|
||||
setShowEndPicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={style["section-title"]}
|
||||
style={{
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>是否启用</span>
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
<div className={style["form-card"]}>
|
||||
<div className={style["enable-section"]}>
|
||||
<span className={style["enable-label"]}>是否启用</span>
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user