内容库可以选择设备

This commit is contained in:
wong
2025-12-04 15:56:19 +08:00
parent b9b1877e04
commit 96caa71f03
8 changed files with 670 additions and 195 deletions

View File

@@ -26,4 +26,5 @@ export interface DeviceSelectionProps {
showSelectedList?: boolean; // 新增 showSelectedList?: boolean; // 新增
readonly?: boolean; // 新增 readonly?: boolean; // 新增
deviceGroups?: any[]; // 传递设备组数据 deviceGroups?: any[]; // 传递设备组数据
singleSelect?: boolean; // 新增,是否单选模式
} }

View File

@@ -18,6 +18,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
showInput = true, showInput = true,
showSelectedList = true, showSelectedList = true,
readonly = false, readonly = false,
singleSelect = false,
}) => { }) => {
// 弹窗控制 // 弹窗控制
const [popupVisible, setPopupVisible] = useState(false); const [popupVisible, setPopupVisible] = useState(false);
@@ -37,6 +38,9 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
// 获取显示文本 // 获取显示文本
const getDisplayText = () => { const getDisplayText = () => {
if (selectedOptions.length === 0) return ""; if (selectedOptions.length === 0) return "";
if (singleSelect && selectedOptions.length > 0) {
return selectedOptions[0].memo || selectedOptions[0].wechatId || "已选择设备";
}
return `已选择 ${selectedOptions.length} 个设备`; return `已选择 ${selectedOptions.length} 个设备`;
}; };
@@ -179,6 +183,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
onClose={() => setRealVisible(false)} onClose={() => setRealVisible(false)}
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
onSelect={onSelect} onSelect={onSelect}
singleSelect={singleSelect}
/> />
</> </>
); );

View File

@@ -12,6 +12,7 @@ interface SelectionPopupProps {
onClose: () => void; onClose: () => void;
selectedOptions: DeviceSelectionItem[]; selectedOptions: DeviceSelectionItem[];
onSelect: (devices: DeviceSelectionItem[]) => void; onSelect: (devices: DeviceSelectionItem[]) => void;
singleSelect?: boolean;
} }
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
@@ -21,6 +22,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
onClose, onClose,
selectedOptions, selectedOptions,
onSelect, onSelect,
singleSelect = false,
}) => { }) => {
// 设备数据 // 设备数据
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]); const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
@@ -110,13 +112,23 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
// 处理设备选择 // 处理设备选择
const handleDeviceToggle = (device: DeviceSelectionItem) => { const handleDeviceToggle = (device: DeviceSelectionItem) => {
if (tempSelectedOptions.some(v => v.id === device.id)) { if (singleSelect) {
setTempSelectedOptions( // 单选模式:如果已选中,则取消选择;否则替换为当前设备
tempSelectedOptions.filter(v => v.id !== device.id), if (tempSelectedOptions.some(v => v.id === device.id)) {
); setTempSelectedOptions([]);
} else {
setTempSelectedOptions([device]);
}
} else { } 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} totalPages={totalPages}
loading={loading} loading={loading}
selectedCount={tempSelectedOptions.length} selectedCount={tempSelectedOptions.length}
singleSelect={singleSelect}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
onCancel={onClose} onCancel={onClose}
onConfirm={() => { onConfirm={() => {
@@ -187,7 +200,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
onClose(); onClose();
}} }}
isAllSelected={isCurrentPageAllSelected} isAllSelected={isCurrentPageAllSelected}
onSelectAll={handleSelectAllCurrentPage} onSelectAll={singleSelect ? undefined : handleSelectAllCurrentPage}
/> />
} }
> >

View File

@@ -95,9 +95,9 @@ export default function FriendSelection({
{(selectedOptions || []).map(friend => ( {(selectedOptions || []).map(friend => (
<div key={friend.id} className={style.selectedListRow}> <div key={friend.id} className={style.selectedListRow}>
<div className={style.selectedListRowContent}> <div className={style.selectedListRowContent}>
<Avatar src={friend.friendAvatar} /> <Avatar src={friend.avatar || friend.friendAvatar} />
<div className={style.selectedListRowContentText}> <div className={style.selectedListRowContentText}>
<div>{friend.friendName}</div> <div>{friend.nickname || friend.friendName}</div>
<div>{friend.wechatId}</div> <div>{friend.wechatId}</div>
</div> </div>
{!readonly && ( {!readonly && (

View File

@@ -14,6 +14,7 @@ interface PopupFooterProps {
// 全选功能相关 // 全选功能相关
isAllSelected?: boolean; isAllSelected?: boolean;
onSelectAll?: (checked: boolean) => void; onSelectAll?: (checked: boolean) => void;
singleSelect?: boolean;
} }
const PopupFooter: React.FC<PopupFooterProps> = ({ const PopupFooter: React.FC<PopupFooterProps> = ({
@@ -26,20 +27,23 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
onConfirm, onConfirm,
isAllSelected = false, isAllSelected = false,
onSelectAll, onSelectAll,
singleSelect = false,
}) => { }) => {
return ( return (
<> <>
{/* 分页栏 */} {/* 分页栏 */}
<div className={style.paginationRow}> <div className={style.paginationRow}>
<div className={style.totalCount}> {onSelectAll && (
<Checkbox <div className={style.totalCount}>
checked={isAllSelected} <Checkbox
onChange={e => onSelectAll(e.target.checked)} checked={isAllSelected}
className={style.selectAllCheckbox} onChange={e => onSelectAll(e.target.checked)}
> className={style.selectAllCheckbox}
>
</Checkbox>
</div> </Checkbox>
</div>
)}
<div className={style.paginationControls}> <div className={style.paginationControls}>
<Button <Button
onClick={() => onPageChange(Math.max(1, currentPage - 1))} onClick={() => onPageChange(Math.max(1, currentPage - 1))}
@@ -61,7 +65,13 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
</div> </div>
</div> </div>
<div className={style.popupFooter}> <div className={style.popupFooter}>
<div className={style.selectedCount}> {selectedCount} </div> <div className={style.selectedCount}>
{singleSelect
? selectedCount > 0
? "已选择设备"
: "未选择设备"
: `已选择 ${selectedCount} 条记录`}
</div>
<div className={style.footerBtnGroup}> <div className={style.footerBtnGroup}>
<Button color="primary" variant="filled" onClick={onCancel}> <Button color="primary" variant="filled" onClick={onCancel}>

View File

@@ -1,23 +1,37 @@
.form-page { .form-page {
background: #f7f8fa; background: #f7f8fa;
padding: 16px; padding: 16px;
padding-bottom: 100px;
} }
.form-main { .form-main {
max-width: 420px; max-width: 420px;
margin: 0 auto; margin: 0 auto;
padding: 16px 0 0 0; padding: 0;
} }
.form-section { .form-section {
margin-bottom: 18px; margin-bottom: 18px;
&:last-child {
margin-bottom: 0;
}
} }
.form-card { .form-card {
border-radius: 16px; border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 24px 18px 18px 18px; padding: 16px;
background: #fff; background: #fff;
margin-bottom: 12px;
}
.source-tag {
font-size: 14px;
padding: 4px 16px;
border-radius: 16px;
margin-top: 8px;
display: inline-block;
} }
.form-label { .form-label {
@@ -32,9 +46,23 @@
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: #222; color: #222;
margin-top: 28px; margin-top: 20px;
margin-bottom: 12px; margin-bottom: 12px;
letter-spacing: 0.5px; 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 { .section-block {
@@ -47,40 +75,78 @@
.adm-tabs-header { .adm-tabs-header {
background: #f7f8fa; background: #f7f8fa;
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 12px;
padding: 4px;
} }
.adm-tabs-tab { .adm-tabs-tab {
font-size: 15px; font-size: 14px;
font-weight: 500; 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 { .keyword-collapse {
margin-top: 12px; .adm-collapse-panel-header {
padding: 0;
background: transparent;
border: none;
}
.adm-collapse-panel-content { .adm-collapse-panel-content {
padding-bottom: 8px; padding: 16px 0 0 0;
background: #f8fafc; background: transparent;
border-radius: 10px;
padding: 18px 14px 10px 14px;
margin-top: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
} }
.form-section { .form-section {
margin-bottom: 22px; margin-bottom: 18px;
&:last-child {
margin-bottom: 0;
}
} }
.form-label { .form-label {
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
margin-bottom: 4px; margin-bottom: 8px;
color: #333; color: #333;
display: block;
} }
.adm-input { }
min-height: 42px;
font-size: 15px; .keyword-header {
border-radius: 7px; display: flex;
margin-bottom: 2px; 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, .ai-row,
@@ -91,9 +157,53 @@
} }
.ai-desc { .ai-desc {
color: #888; color: #666;
font-size: 13px; font-size: 14px;
flex: 1; 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, .date-row,
@@ -109,6 +219,179 @@
border-radius: 8px; 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 { .submit-btn {
margin-top: 32px; margin-top: 32px;
height: 48px !important; height: 48px !important;

View File

@@ -1,16 +1,19 @@
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 { Input as AntdInput, Switch } from "antd"; import { Input as AntdInput, Switch, Tag } from "antd";
import { Button, Collapse, Toast, DatePicker, Tabs } from "antd-mobile"; import { Button, Collapse, Toast, DatePicker, Tabs } from "antd-mobile";
import { DownOutlined } from "@ant-design/icons";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import FriendSelection from "@/components/FriendSelection"; import FriendSelection from "@/components/FriendSelection";
import GroupSelection from "@/components/GroupSelection"; import GroupSelection from "@/components/GroupSelection";
import DeviceSelection from "@/components/DeviceSelection";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss"; import style from "./index.module.scss";
import request from "@/api/request"; import request from "@/api/request";
import { getContentLibraryDetail, updateContentLibrary } from "./api"; import { getContentLibraryDetail, updateContentLibrary } from "./api";
import { GroupSelectionItem } from "@/components/GroupSelection/data"; import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { FriendSelectionItem } from "@/components/FriendSelection/data";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
const { TextArea } = AntdInput; const { TextArea } = AntdInput;
@@ -29,6 +32,15 @@ export default function ContentForm() {
const isEdit = !!id; const isEdit = !!id;
const [sourceType, setSourceType] = useState<"friends" | "groups">("friends"); const [sourceType, setSourceType] = useState<"friends" | "groups">("friends");
const [name, setName] = useState(""); 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 [friendsGroups, setSelectedFriends] = useState<string[]>([]);
const [friendsGroupsOptions, setSelectedFriendsOptions] = useState< const [friendsGroupsOptions, setSelectedFriendsOptions] = useState<
FriendSelectionItem[] FriendSelectionItem[]
@@ -64,6 +76,23 @@ export default function ContentForm() {
.then(data => { .then(data => {
setName(data.name || ""); setName(data.name || "");
setSourceType(data.sourceType === 1 ? "friends" : "groups"); 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 || []); setSelectedFriends(data.sourceFriends || []);
setSelectedGroups(data.selectedGroups || []); setSelectedGroups(data.selectedGroups || []);
setSelectedGroupsOptions(data.selectedGroupsOptions || []); setSelectedGroupsOptions(data.selectedGroupsOptions || []);
@@ -72,7 +101,13 @@ export default function ContentForm() {
setKeywordsExclude((data.keywordExclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(","));
setCatchType(data.catchType || ["text", "image", "video"]); setCatchType(data.catchType || ["text", "image", "video"]);
setAIPrompt(data.aiPrompt || ""); 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); setEnabled(data.status === 1);
// 时间范围 // 时间范围
const start = data.timeStart || data.startTime; const start = data.timeStart || data.startTime;
@@ -103,6 +138,7 @@ export default function ContentForm() {
const payload = { const payload = {
name, name,
sourceType: sourceType === "friends" ? 1 : 2, sourceType: sourceType === "friends" ? 1 : 2,
devices: selectedDevices.map(d => d.id),
friendsGroups: friendsGroups, friendsGroups: friendsGroups,
wechatGroups: selectedGroups, wechatGroups: selectedGroups,
groupMembers: {}, groupMembers: {},
@@ -115,7 +151,8 @@ export default function ContentForm() {
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean), .filter(Boolean),
catchType, catchType,
aiPrompt, aiPrompt,
aiEnabled: useAI ? 1 : 0,
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0, timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
startTime: dateRange[0] ? formatDate(dateRange[0]) : "", startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
endTime: dateRange[1] ? formatDate(dateRange[1]) : "", endTime: dateRange[1] ? formatDate(dateRange[1]) : "",
@@ -178,7 +215,7 @@ export default function ContentForm() {
onSubmit={e => e.preventDefault()} onSubmit={e => e.preventDefault()}
autoComplete="off" autoComplete="off"
> >
<div className={style["form-section"]}> <div className={style["form-card"]}>
<label className={style["form-label"]}> <label className={style["form-label"]}>
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span> <span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
@@ -191,8 +228,100 @@ export default function ContentForm() {
/> />
</div> </div>
<div className={style["section-title"]}></div> <div className={style["form-card"]}>
<div className={style["form-section"]}> <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 <Tabs
activeKey={sourceType} activeKey={sourceType}
onChange={key => setSourceType(key as "friends" | "groups")} onChange={key => setSourceType(key as "friends" | "groups")}
@@ -203,6 +332,7 @@ export default function ContentForm() {
selectedOptions={friendsGroupsOptions} selectedOptions={friendsGroupsOptions}
onSelect={handleFriendsChange} onSelect={handleFriendsChange}
placeholder="选择微信好友" placeholder="选择微信好友"
deviceIds={selectedDevices.map(d => Number(d.id))}
/> />
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab title="选择聊天群" key="groups"> <Tabs.Tab title="选择聊天群" key="groups">
@@ -215,54 +345,53 @@ export default function ContentForm() {
</Tabs> </Tabs>
</div> </div>
<Collapse className={style["collapse"]}> <div className={style["form-card"]}>
<Collapse.Panel <Collapse className={style["keyword-collapse"]}>
key="keywords" <Collapse.Panel
title={<span className={style["form-label"]}></span>} key="keywords"
> title={
<div className={style["form-section"]}> <div className={style["keyword-header"]}>
<label className={style["form-label"]}></label> <span className={style["keyword-title"]}></span>
<TextArea <DownOutlined className={style["keyword-arrow"]} />
placeholder="多个关键词用逗号分隔" </div>
value={keywordsInclude} }
onChange={e => setKeywordsInclude(e.target.value)} >
className={style["input"]} <div className={style["form-section"]}>
autoSize={{ minRows: 2, maxRows: 4 }} <label className={style["form-label"]}></label>
/> <TextArea
</div> placeholder="多个关键词用逗号分隔"
<div className={style["form-section"]}> value={keywordsInclude}
<label className={style["form-label"]}></label> onChange={e => setKeywordsInclude(e.target.value)}
<TextArea className={style["input"]}
placeholder="多个关键词用逗号分隔" autoSize={{ minRows: 2, maxRows: 4 }}
value={keywordsExclude} />
onChange={e => setKeywordsExclude(e.target.value)} </div>
className={style["input"]} <div className={style["form-section"]}>
autoSize={{ minRows: 2, maxRows: 4 }} <label className={style["form-label"]}></label>
/> <TextArea
</div> placeholder="多个关键词用逗号分隔"
</Collapse.Panel> value={keywordsExclude}
</Collapse> 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-card"]}>
<div className={style["form-section"]}> <div className={style["content-type-header"]}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 12 }}> <span className={style["content-type-title"]}></span>
</div>
<div className={style["content-type-buttons"]}>
{["text", "image", "video"].map(type => ( {["text", "image", "video"].map(type => (
<div <button
key={type} key={type}
style={{ className={`${style["content-type-btn"]} ${
display: "flex", catchType.includes(type) ? style["active"] : ""
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",
}}
onClick={() => { onClick={() => {
setCatchType(prev => setCatchType(prev =>
prev.includes(type) prev.includes(type)
@@ -271,100 +400,101 @@ export default function ContentForm() {
); );
}} }}
> >
<span> {type === "text"
{type === "text" ? "文本"
? "文本" : type === "image"
: type === "image" ? "图片"
? "图片" : "视频"}
: "视频"} </button>
</span>
</div>
))} ))}
</div> </div>
</div> </div>
<div className={style["section-title"]}>AI</div> <div className={style["form-card"]}>
<div <div
className={style["form-section"]} className={style["form-section"]}
style={{ display: "flex", alignItems: "center", gap: 12 }} style={{ display: "flex", alignItems: "center", gap: 12 }}
> >
<Switch checked={useAI} onChange={setUseAI} /> <Switch checked={useAI} onChange={setUseAI} />
<span className={style["ai-desc"]}> <span className={style["ai-desc"]}>
AI后AI生成 ,AI生成
</span> </span>
</div> </div>
{useAI && ( {useAI && (
<div className={style["form-section"]}> <div className={style["form-section"]}>
<label className={style["form-label"]}>AI提示词</label> <label className={style["form-label"]}>AI提示词</label>
<TextArea <TextArea
placeholder="请输入AI提示词" placeholder="请输入AI提示词"
value={aiPrompt} value={aiPrompt}
onChange={e => setAIPrompt(e.target.value)} onChange={e => setAIPrompt(e.target.value)}
className={style["input"]} className={style["input"]}
autoSize={{ minRows: 4, maxRows: 10 }} autoSize={{ minRows: 4, maxRows: 10 }}
/> />
</div> </div>
)} )}
</div>
<div className={style["section-title"]}></div> <div className={style["form-card"]}>
<div <div className={style["time-limit-header"]}>
className={style["form-section"]} <span className={style["time-limit-title"]}></span>
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> </div>
<label></label> <div className={style["date-inputs"]}>
<div style={{ flex: 1 }}> <div className={style["date-item"]}>
<AntdInput <label className={style["date-label"]}></label>
readOnly <AntdInput
value={dateRange[1] ? dateRange[1].toLocaleDateString() : ""} readOnly
placeholder="年/月/日" value={
className={style["input"]} dateRange[0]
onClick={() => setShowEndPicker(true)} ? `${dateRange[0].getFullYear()}/${String(dateRange[0].getMonth() + 1).padStart(2, "0")}/${String(dateRange[0].getDate()).padStart(2, "0")}`
/> : ""
<DatePicker }
visible={showEndPicker} placeholder="年/月/日"
title="结束时间" className={style["date-input"]}
value={dateRange[1]} onClick={() => setShowStartPicker(true)}
onClose={() => setShowEndPicker(false)} />
onConfirm={val => { <DatePicker
setDateRange([dateRange[0], val]); visible={showStartPicker}
setShowEndPicker(false); 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> </div>
<div <div className={style["form-card"]}>
className={style["section-title"]} <div className={style["enable-section"]}>
style={{ <span className={style["enable-label"]}></span>
marginTop: 24, <Switch checked={enabled} onChange={setEnabled} />
marginBottom: 8, </div>
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span></span>
<Switch checked={enabled} onChange={setEnabled} />
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,6 +2,9 @@
namespace app\cunkebao\controller; namespace app\cunkebao\controller;
use app\common\model\Device as DeviceModel;
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
use app\common\model\WechatCustomer as WechatCustomerModel;
use app\cunkebao\model\ContentLibrary; use app\cunkebao\model\ContentLibrary;
use app\cunkebao\model\ContentItem; use app\cunkebao\model\ContentItem;
use library\s2\titleFavicon; use library\s2\titleFavicon;
@@ -64,6 +67,7 @@ class ContentLibraryController extends Controller
$keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]); $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]);
$keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]); $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]);
$devices = isset($param['devices']) ? json_encode($param['devices'], 256) : json_encode([]);
$sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1; $sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1;
@@ -75,6 +79,7 @@ class ContentLibraryController extends Controller
'sourceGroups' => $sourceType == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]), // 选择的微信群 'sourceGroups' => $sourceType == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]), // 选择的微信群
'groupMembers' => $sourceType == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]), // 群组成员 'groupMembers' => $sourceType == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]), // 群组成员
'catchType' => isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]), // 采集类型 'catchType' => isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]), // 采集类型
'devices' => $devices,
// 关键词配置 // 关键词配置
'keywordInclude' => $keywordInclude, // 包含的关键词 'keywordInclude' => $keywordInclude, // 包含的关键词
'keywordExclude' => $keywordExclude, // 排除的关键词 'keywordExclude' => $keywordExclude, // 排除的关键词
@@ -314,7 +319,7 @@ class ContentLibraryController extends Controller
// 查询内容库信息 // 查询内容库信息
$library = ContentLibrary::where($where) $library = ContentLibrary::where($where)
->field('id,name,sourceType,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers,catchType') ->field('id,name,sourceType,devices ,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers,catchType')
->find(); ->find();
if (empty($library)) { if (empty($library)) {
@@ -322,13 +327,14 @@ class ContentLibraryController extends Controller
} }
// 处理JSON字段转数组 // 处理JSON字段转数组
$library['friendsGroups'] = json_decode($library['sourceFriends'] ?: '[]', true); $library['friendsGroups'] = json_decode($library['sourceFriends'] ?: [], true);
$library['wechatGroups'] = json_decode($library['sourceGroups'] ?: '[]', true); $library['wechatGroups'] = json_decode($library['sourceGroups'] ?: [], true);
$library['keywordInclude'] = json_decode($library['keywordInclude'] ?: '[]', true); $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: [], true);
$library['keywordExclude'] = json_decode($library['keywordExclude'] ?: '[]', true); $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: [], true);
$library['groupMembers'] = json_decode($library['groupMembers'] ?: '[]', true); $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true);
$library['catchType'] = json_decode($library['catchType'] ?: '[]', true); $library['catchType'] = json_decode($library['catchType'] ?: [], true);
unset($library['sourceFriends'], $library['sourceGroups']); $library['deviceGroups'] = json_decode($library['devices'] ?: [], true);
unset($library['sourceFriends'], $library['sourceGroups'],$library['devices']);
// 将时间戳转换为日期格式(精确到日) // 将时间戳转换为日期格式(精确到日)
if (!empty($library['timeStart'])) { if (!empty($library['timeStart'])) {
@@ -369,6 +375,32 @@ class ContentLibraryController extends Controller
} }
} }
//获取设备信息
if (!empty($library['deviceGroups'])) {
$deviceList = DeviceModel::alias('d')
->field([
'd.id', 'd.imei', 'd.memo', 'd.alive',
'l.wechatId',
'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
])
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
->whereIn('d.id', $library['deviceGroups'])
->order('d.id desc')
->select();
foreach ($deviceList as &$device) {
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
}
unset($device);
$library['deviceGroupsOptions'] = $deviceList;
} else {
$library['deviceGroupsOptions'] = [];
}
return json([ return json([
'code' => 200, 'code' => 200,
'msg' => '获取成功', 'msg' => '获取成功',
@@ -401,7 +433,8 @@ class ContentLibraryController extends Controller
$where = [ $where = [
['companyId', '=', $this->request->userInfo['companyId']], ['companyId', '=', $this->request->userInfo['companyId']],
['isDel', '=', 0] // 只查询未删除的记录 ['isDel', '=', 0], // 只查询未删除的记录
['id', '=', $param['id']]
]; ];
if (empty($this->request->userInfo['isAdmin'])) { if (empty($this->request->userInfo['isAdmin'])) {
@@ -420,6 +453,7 @@ class ContentLibraryController extends Controller
$keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]); $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]);
$keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]); $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]);
$devices = isset($param['devices']) ? json_encode($param['devices'], 256) : json_encode([]);
// 更新内容库基本信息 // 更新内容库基本信息
$library->name = $param['name']; $library->name = $param['name'];
@@ -428,6 +462,7 @@ class ContentLibraryController extends Controller
$library->sourceGroups = $param['sourceType'] == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]); $library->sourceGroups = $param['sourceType'] == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]);
$library->groupMembers = $param['sourceType'] == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]); $library->groupMembers = $param['sourceType'] == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]);
$library->catchType = isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]);// 采集类型 $library->catchType = isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]);// 采集类型
$library->devices = $devices;
$library->keywordInclude = $keywordInclude; $library->keywordInclude = $keywordInclude;
$library->keywordExclude = $keywordExclude; $library->keywordExclude = $keywordExclude;
$library->aiEnabled = isset($param['aiEnabled']) ? $param['aiEnabled'] : 0; $library->aiEnabled = isset($param['aiEnabled']) ? $param['aiEnabled'] : 0;
@@ -437,8 +472,6 @@ class ContentLibraryController extends Controller
$library->timeEnd = isset($param['endTime']) ? strtotime($param['endTime']) : 0; $library->timeEnd = isset($param['endTime']) ? strtotime($param['endTime']) : 0;
$library->status = isset($param['status']) ? $param['status'] : 0; $library->status = isset($param['status']) ? $param['status'] : 0;
$library->updateTime = time(); $library->updateTime = time();
$library->save(); $library->save();
Db::commit(); Db::commit();
@@ -597,8 +630,8 @@ class ContentLibraryController extends Controller
$item['content'] = !empty($item['contentAi']) ? $item['contentAi'] : $item['content']; $item['content'] = !empty($item['contentAi']) ? $item['contentAi'] : $item['content'];
// 处理JSON字段 // 处理JSON字段
$item['resUrls'] = json_decode($item['resUrls'] ?: '[]', true); $item['resUrls'] = json_decode($item['resUrls'] ?: [], true);
$item['urls'] = json_decode($item['urls'] ?: '[]', true); $item['urls'] = json_decode($item['urls'] ?: [], true);
// 格式化时间 // 格式化时间
if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) { if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) {
@@ -798,8 +831,8 @@ class ContentLibraryController extends Controller
} }
// 处理JSON字段 // 处理JSON字段
$item['resUrls'] = json_decode($item['resUrls'] ?: '[]', true); $item['resUrls'] = json_decode($item['resUrls'] ?: [], true);
$item['urls'] = json_decode($item['urls'] ?: '[]', true); $item['urls'] = json_decode($item['urls'] ?: [], true);
// 添加内容类型的文字描述 // 添加内容类型的文字描述
$contentTypeMap = [ $contentTypeMap = [
@@ -1083,12 +1116,12 @@ class ContentLibraryController extends Controller
// 预处理内容库数据 // 预处理内容库数据
foreach ($libraries as &$library) { foreach ($libraries as &$library) {
// 解析JSON字段 // 解析JSON字段
$library['sourceFriends'] = json_decode($library['sourceFriends'] ?: '[]', true); $library['sourceFriends'] = json_decode($library['sourceFriends'] ?: [], true);
$library['sourceGroups'] = json_decode($library['sourceGroups'] ?: '[]', true); $library['sourceGroups'] = json_decode($library['sourceGroups'] ?: [], true);
$library['keywordInclude'] = json_decode($library['keywordInclude'] ?: '[]', true); $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: [], true);
$library['keywordExclude'] = json_decode($library['keywordExclude'] ?: '[]', true); $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: [], true);
$library['groupMembers'] = json_decode($library['groupMembers'] ?: '[]', true); $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true);
$library['catchType'] = json_decode($library['catchType'] ?: '[]', true); $library['catchType'] = json_decode($library['catchType'] ?: [], true);
} }
unset($library); // 解除引用 unset($library); // 解除引用