diff --git a/Cunkebao/dist/.vite/manifest.json b/Cunkebao/dist/.vite/manifest.json index fabab32f..2289b6a9 100644 --- a/Cunkebao/dist/.vite/manifest.json +++ b/Cunkebao/dist/.vite/manifest.json @@ -1,50 +1,50 @@ { - "_charts-BjEBSMrK.js": { - "file": "assets/charts-BjEBSMrK.js", + "_charts-aNYyX7D2.js": { + "file": "assets/charts-aNYyX7D2.js", "name": "charts", "imports": [ - "_ui-CiJ_pikt.js", - "_vendor-BPPoWDlG.js" - ] - }, - "_ui-CiJ_pikt.js": { - "file": "assets/ui-CiJ_pikt.js", - "name": "ui", - "imports": [ - "_vendor-BPPoWDlG.js" - ], - "css": [ - "assets/ui-D0C0OGrH.css" + "_ui-DZwp85UP.js", + "_vendor-Bq99rrm8.js" ] }, "_ui-D0C0OGrH.css": { "file": "assets/ui-D0C0OGrH.css", "src": "_ui-D0C0OGrH.css" }, - "_utils-DiZV3oaL.js": { - "file": "assets/utils-DiZV3oaL.js", - "name": "utils", + "_ui-DZwp85UP.js": { + "file": "assets/ui-DZwp85UP.js", + "name": "ui", "imports": [ - "_vendor-BPPoWDlG.js" + "_vendor-Bq99rrm8.js" + ], + "css": [ + "assets/ui-D0C0OGrH.css" ] }, - "_vendor-BPPoWDlG.js": { - "file": "assets/vendor-BPPoWDlG.js", + "_utils-Ft3ushmX.js": { + "file": "assets/utils-Ft3ushmX.js", + "name": "utils", + "imports": [ + "_vendor-Bq99rrm8.js" + ] + }, + "_vendor-Bq99rrm8.js": { + "file": "assets/vendor-Bq99rrm8.js", "name": "vendor" }, "index.html": { - "file": "assets/index-BesOjMPu.js", + "file": "assets/index-CCIZs36L.js", "name": "index", "src": "index.html", "isEntry": true, "imports": [ - "_vendor-BPPoWDlG.js", - "_utils-DiZV3oaL.js", - "_ui-CiJ_pikt.js", - "_charts-BjEBSMrK.js" + "_vendor-Bq99rrm8.js", + "_ui-DZwp85UP.js", + "_utils-Ft3ushmX.js", + "_charts-aNYyX7D2.js" ], "css": [ - "assets/index-677RgwmW.css" + "assets/index-DRrzDMi4.css" ] } } \ No newline at end of file diff --git a/Cunkebao/dist/index.html b/Cunkebao/dist/index.html index 16e6d604..9b257a9d 100644 --- a/Cunkebao/dist/index.html +++ b/Cunkebao/dist/index.html @@ -11,13 +11,13 @@ - - - - - + + + + + - +
diff --git a/Cunkebao/src/main.tsx b/Cunkebao/src/main.tsx index 813ccaed..e442b9c1 100644 --- a/Cunkebao/src/main.tsx +++ b/Cunkebao/src/main.tsx @@ -1,10 +1,17 @@ // main.tsx import React from "react"; import { createRoot } from "react-dom/client"; +import { ConfigProvider } from "antd"; +import zhCN from "antd/locale/zh_CN"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-cn"; import App from "./App"; import "./styles/global.scss"; import { db } from "@/utils/db"; // 引入数据库实例 +// 设置dayjs为中文 +dayjs.locale("zh-cn"); + // 数据库初始化 async function initializeApp() { try { @@ -26,7 +33,11 @@ async function initializeApp() { // 渲染应用 const root = createRoot(document.getElementById("root")!); - root.render(); + root.render( + + + , + ); } // 启动应用 diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index 0082d02c..2aed0816 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -48,6 +48,11 @@ export default function ContentForm() { const [showEndPicker, setShowEndPicker] = useState(false); const [keywordsInclude, setKeywordsInclude] = useState(""); const [keywordsExclude, setKeywordsExclude] = useState(""); + const [catchType, setCatchType] = useState([ + "text", + "image", + "video", + ]); const [submitting, setSubmitting] = useState(false); const [loading, setLoading] = useState(false); @@ -65,6 +70,7 @@ export default function ContentForm() { setSelectedFriendsOptions(data.friendsGroupsOptions || []); setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(",")); + setCatchType(data.catchType || ["text", "image", "video"]); setAIPrompt(data.aiPrompt || ""); setUseAI(!!data.aiPrompt); setEnabled(data.status === 1); @@ -108,6 +114,7 @@ export default function ContentForm() { .split(/,|,|\n|\s+/) .map(s => s.trim()) .filter(Boolean), + catchType, aiPrompt, timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0, startTime: dateRange[0] ? formatDate(dateRange[0]) : "", @@ -236,6 +243,46 @@ export default function ContentForm() { + {/* 采集内容类型 */} +
采集内容类型
+
+
+ {["text", "image", "video"].map(type => ( +
{ + setCatchType(prev => + prev.includes(type) + ? prev.filter(t => t !== type) + : [...prev, type], + ); + }} + > + + {type === "text" + ? "文本" + : type === "image" + ? "图片" + : "视频"} + +
+ ))} +
+
+
是否启用AI
{ const total = list.length; - const highValue = list.filter(u => u.tags.includes("高价值客户池")).length; + const highValue = list.filter( + u => u.tags && u.tags.includes("高价值客户池"), + ).length; const added = list.filter(u => u.status === 1).length; const pending = list.filter(u => u.status === 0).length; const failed = list.filter(u => u.status === -1).length; @@ -64,6 +66,11 @@ export function useTrafficPoolListLogic() { const res = await fetchTrafficPoolList(params); setList(res.list || []); setTotal(res.total || 0); + } catch (error) { + // 忽略请求过于频繁的错误,避免页面崩溃 + if (error !== "请求过于频繁,请稍后再试") { + console.error("获取列表失败:", error); + } } finally { setLoading(false); } diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx index e3bacf8f..0c08b5e6 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback, useEffect, useState } from "react"; import Layout from "@/components/Layout/Layout"; import { SearchOutlined, @@ -25,7 +25,6 @@ const TrafficPoolList: React.FC = () => { list, page, setPage, - pageSize, total, search, setSearch, @@ -52,6 +51,22 @@ const TrafficPoolList: React.FC = () => { getList, } = useTrafficPoolListLogic(); + // 搜索防抖处理 + const [searchInput, setSearchInput] = useState(search); + + const debouncedSearch = useCallback(() => { + const timer = setTimeout(() => { + setSearch(searchInput); + }, 500); // 500ms 防抖延迟 + + return () => clearTimeout(timer); + }, [searchInput, setSearch]); + + useEffect(() => { + const cleanup = debouncedSearch(); + return cleanup; + }, [debouncedSearch]); + return ( {
setSearch(e.target.value)} + value={searchInput} + onChange={e => setSearchInput(e.target.value)} prefix={} allowClear size="large" diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/DeviceListModal.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/DeviceListModal.tsx index 111483cd..e220e3fc 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/DeviceListModal.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/DeviceListModal.tsx @@ -38,7 +38,7 @@ const DeviceListModal: React.FC = ({ setLoading(true); try { const detailRes = await getPlanDetail(ruleId.toString()); - const deviceData = detailRes?.deveiceGroupsOptions || []; + const deviceData = detailRes?.deviceGroupsOptions || []; setDevices(deviceData); } catch (error) { console.error("获取设备详情失败:", error); diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts index 34489536..3cd21563 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts @@ -25,8 +25,8 @@ export interface FormData { device: string[]; customTags: string[]; customTagsOptions: string[]; - deveiceGroups: string[]; - deveiceGroupsOptions: DeviceSelectionItem[]; + deviceGroups: string[]; + deviceGroupsOptions: DeviceSelectionItem[]; wechatGroups: string[]; wechatGroupsOptions: GroupSelectionItem[]; messagePlans: any[]; @@ -50,8 +50,8 @@ export const defFormData: FormData = { customTags: [], customTagsOptions: [], messagePlans: [], - deveiceGroups: [], - deveiceGroupsOptions: [], + deviceGroups: [], + deviceGroupsOptions: [], wechatGroups: [], wechatGroupsOptions: [], contentGroups: [], diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx index acce83fa..949574a1 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -67,8 +67,8 @@ export default function NewPlan() { remarkFormat: detail.remarkFormat ?? "", addFriendInterval: detail.addFriendInterval ?? 1, tips: detail.tips ?? "", - deveiceGroups: detail.deveiceGroups ?? [], - deveiceGroupsOptions: detail.deveiceGroupsOptions ?? [], + deviceGroups: detail.deviceGroups ?? [], + deviceGroupsOptions: detail.deviceGroupsOptions ?? [], wechatGroups: detail.wechatGroups ?? [], wechatGroupsOptions: detail.wechatGroupsOptions ?? [], contentGroups: detail.contentGroups ?? [], diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx index 5aa73aed..db8f3648 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -76,11 +76,11 @@ const FriendRequestSettings: React.FC = ({ onChange({ ...formData, greeting: template }); setIsTemplateDialogOpen(false); }; - const handleDevicesChange = (deveiceGroupsOptions: DeviceSelectionItem[]) => { + const handleDevicesChange = (deviceGroupsOptions: DeviceSelectionItem[]) => { onChange({ ...formData, - deveiceGroups: deveiceGroupsOptions.map(d => d.id), - deveiceGroupsOptions: deveiceGroupsOptions, + deviceGroups: deviceGroupsOptions.map(d => d.id), + deviceGroupsOptions: deviceGroupsOptions, }); }; @@ -90,7 +90,7 @@ const FriendRequestSettings: React.FC = ({
选择设备
diff --git a/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/DeviceSelector.tsx b/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/DeviceSelector.tsx index d3b3bb16..281dbab2 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/DeviceSelector.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/DeviceSelector.tsx @@ -6,8 +6,8 @@ import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; interface DeviceSelectorProps { selectedDevices: DeviceSelectionItem[]; onNext: (data: { - deveiceGroups: string[]; - deveiceGroupsOptions: DeviceSelectionItem[]; + deviceGroups: string[]; + deviceGroupsOptions: DeviceSelectionItem[]; }) => void; } @@ -37,15 +37,13 @@ const DeviceSelector = forwardRef( })); // 设备选择 - const handleDeviceSelect = ( - deveiceGroupsOptions: DeviceSelectionItem[], - ) => { - const deveiceGroups = deveiceGroupsOptions.map(item => item.id); - form.setFieldValue("deveiceGroups", deveiceGroups); + const handleDeviceSelect = (deviceGroupsOptions: DeviceSelectionItem[]) => { + const deviceGroups = deviceGroupsOptions.map(item => item.id); + form.setFieldValue("deviceGroups", deviceGroups); // 通知父组件数据变化 onNext({ - deveiceGroups: deveiceGroups.map(id => String(id)), - deveiceGroupsOptions, + deviceGroups: deviceGroups.map(id => String(id)), + deviceGroupsOptions, }); }; @@ -55,7 +53,7 @@ const DeviceSelector = forwardRef( form={form} layout="vertical" initialValues={{ - deveiceGroups: selectedDevices.map(item => item.id), + deviceGroups: selectedDevices.map(item => item.id), }} >
@@ -68,7 +66,7 @@ const DeviceSelector = forwardRef(
{ const updatedForm = { ...defaultForm, name: res.name, - deveiceGroups: res.config.deveiceGroups || [], - deveiceGroupsOptions: res.config.deveiceGroupsOptions || [], + deviceGroups: res.config.deviceGroups || [], + deviceGroupsOptions: res.config.deviceGroupsOptions || [], poolGroups: res.config.poolGroups || [], poolGroupsOptions: res.config.poolGroupsOptions || [], startTime: res.config.startTime, @@ -80,7 +80,7 @@ const AutoGroupForm: React.FC = () => { id: res.id, }; setFormData(updatedForm); - setDeviceGroupsOptions(res.config.deveiceGroupsOptions || []); + setDeviceGroupsOptions(res.config.deviceGroupsOptions || []); setpoolGroupsOptions(res.config.poolGroupsOptions || []); setDataLoaded(true); // 标记数据已加载 }); @@ -92,14 +92,14 @@ const AutoGroupForm: React.FC = () => { // 设备组选择 const handleDevicesChange = (data: { - deveiceGroups: string[]; - deveiceGroupsOptions: DeviceSelectionItem[]; + deviceGroups: string[]; + deviceGroupsOptions: DeviceSelectionItem[]; }) => { setFormData(prev => ({ ...prev, - deveiceGroups: data.deveiceGroups, + deviceGroups: data.deviceGroups, })); - setDeviceGroupsOptions(data.deveiceGroupsOptions); + setDeviceGroupsOptions(data.deviceGroupsOptions); }; // 流量池包选择 @@ -116,7 +116,7 @@ const AutoGroupForm: React.FC = () => { Toast.show({ content: "请输入任务名称" }); return; } - if (formData.deveiceGroups.length === 0) { + if (formData.deviceGroups.length === 0) { Toast.show({ content: "请选择至少一个设备组" }); return; } @@ -129,7 +129,7 @@ const AutoGroupForm: React.FC = () => { try { const submitData = { ...formData, - deveiceGroupsOptions: deviceGroupsOptions, + deviceGroupsOptions: deviceGroupsOptions, poolGroupsOptions: poolGroupsOptions, }; diff --git a/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts b/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts index 83ad789d..ba9cbf8a 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts @@ -6,8 +6,8 @@ export interface AutoGroupFormData { id?: string; // 任务ID type: number; // 任务类型 name: string; // 任务名称 - deveiceGroups: string[]; // 设备组 - deveiceGroupsOptions: DeviceSelectionItem[]; // 设备组选项 + deviceGroups: string[]; // 设备组 + deviceGroupsOptions: DeviceSelectionItem[]; // 设备组选项 poolGroups: string[]; // 流量池 poolGroupsOptions: PoolSelectionItem[]; // 流量池选项 startTime: string; // 开始时间 (YYYY-MM-DD HH:mm:ss) @@ -34,7 +34,7 @@ export const formValidationRules = { { required: true, message: "请输入任务名称" }, { min: 2, max: 50, message: "任务名称长度应在2-50个字符之间" }, ], - deveiceGroups: [ + deviceGroups: [ { required: true, message: "请选择设备组" }, { type: "array", min: 1, message: "至少选择一个设备组" }, ], diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts b/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts index 43f1da6f..1b47baee 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts @@ -79,8 +79,8 @@ export interface CreateLikeTaskData { startTime: string; endTime: string; contentTypes: ContentType[]; - deveiceGroups: number[]; - deveiceGroupsOptions: DeviceSelectionItem[]; + deviceGroups: number[]; + deviceGroupsOptions: DeviceSelectionItem[]; friendsGroups: number[]; friendsGroupsOptions: FriendSelectionItem[]; friendMaxLikes: number; diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx index 3d0aca8d..779aecea 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx @@ -43,8 +43,8 @@ const NewAutoLike: React.FC = () => { startTime: "08:00", endTime: "22:00", contentTypes: ["text", "image", "video"], - deveiceGroups: [], - deveiceGroupsOptions: [], + deviceGroups: [], + deviceGroupsOptions: [], friendsGroups: [], friendsGroupsOptions: [], targetTags: [], @@ -72,8 +72,8 @@ const NewAutoLike: React.FC = () => { startTime: config.timeRange?.start || config.startTime || "08:00", endTime: config.timeRange?.end || config.endTime || "22:00", contentTypes: config.contentTypes || ["text", "image", "video"], - deveiceGroups: config.deveiceGroups || [], - deveiceGroupsOptions: config.deveiceGroupsOptions || [], + deviceGroups: config.deviceGroups || [], + deviceGroupsOptions: config.deviceGroupsOptions || [], friendsGroups: config.friendsgroups || [], friendsGroupsOptions: config.friendsGroupsOptions || [], targetTags: config.targetTags || [], @@ -121,7 +121,7 @@ const NewAutoLike: React.FC = () => { message.warning("请输入任务名称"); return; } - if (!formData.deveiceGroups || formData.deveiceGroups.length === 0) { + if (!formData.deviceGroups || formData.deviceGroups.length === 0) { message.warning("请选择执行设备"); return; } @@ -329,11 +329,11 @@ const NewAutoLike: React.FC = () => {
handleUpdateFormData({ - deveiceGroups: devices.map(v => v.id), - deveiceGroupsOptions: devices, + deviceGroups: devices.map(v => v.id), + deviceGroupsOptions: devices, }) } showInput={true} @@ -353,7 +353,7 @@ const NewAutoLike: React.FC = () => { onClick={handleNext} className={style.nextBtn} size="large" - disabled={formData.deveiceGroups.length === 0} + disabled={formData.deviceGroups.length === 0} > 下一步 @@ -372,7 +372,7 @@ const NewAutoLike: React.FC = () => { friendsGroupsOptions: friends, }) } - deviceIds={formData.deveiceGroups} + deviceIds={formData.deviceGroups} />
+ } + title="任务详情" + /> + } + > +
+ 加载中... +
+ + ); + } + + if (!task) { + return ( + navigate("/workspace/contact-import/list")} + > + 返回 + + } + title="任务详情" + /> + } + > +
+ +
+
+ ); + } + + const statusInfo = getStatusInfo(task.status); + + return ( + + + navigate(`/workspace/contact-import/form/${task.id}`) + } + > + 编辑 + + } + /> + {/* 任务操作栏 */} + +
+
+
{task.name}
+
+ {statusInfo.text} +
+
+
+
+ + +
+
+ + } + > + +
+ {/* 标签页 */} + + +
+ {/* 基本信息 */} + +
基本信息
+
+
+ 任务名称: + {task.name} +
+
+ 设备组数: + + {task.deviceGroups?.length || 0} + +
+
+ 导入数量: + {task.num} +
+
+ 客户端ID: + {task.clientId} +
+
+ 备注类型: + {task.remarkType} +
+
+ 备注内容: + {task.remarkValue} +
+
+
+ + {/* 时间配置 */} + +
时间配置
+
+
+ 开始时间: + {task.startTime} +
+
+ 结束时间: + {task.endTime} +
+
+ 每日最大导入: + + {task.maxImportsPerDay} + +
+
+ 导入间隔: + + {task.importInterval}分钟 + +
+
+
+ + {/* 统计信息 */} + +
统计信息
+
+
+ 今日导入: + + {task.todayImportCount} + +
+
+ 总导入数: + + {task.totalImportCount} + +
+
+ 创建时间: + {task.createTime} +
+
+ 更新时间: + {task.updateTime} +
+
+
+
+
+ + +
+ {records.length === 0 && !recordsLoading ? ( + + ) : ( + + {records.map(record => ( + +
+
+ + {getRecordStatusIcon(record.importStatus)} + + {record.deviceName} + + {getRecordStatusTag(record.importStatus)} + +
+
+ {record.createTime} +
+
+
+
+ 导入数量: + {record.num} +
+
+ 备注: + + {record.remarkType}: {record.remarkValue} + +
+ {record.errorMessage && ( +
+ 错误信息: {record.errorMessage} +
+ )} +
+
+ ))} +
+ )} + + + {recordsLoading && ( +
+ 加载中... +
+ )} +
+
+
+
+
+
+
+ ); +}; + +export default ContactImportDetail; diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/form/api.ts b/Cunkebao/src/pages/mobile/workspace/contact-import/form/api.ts new file mode 100644 index 00000000..d9abded4 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/form/api.ts @@ -0,0 +1,91 @@ +import request from "@/api/request"; +import { + Allocation, + CreateContactImportTaskData, + UpdateContactImportTaskData, + ContactImportRecord, + PaginatedResponse, + ImportStats, +} from "./data"; + +// 获取通讯录导入任务列表 +export function fetchContactImportTasks( + params = { type: 6, page: 1, limit: 10 }, +) { + return request("/v1/workbench/list", params, "GET"); +} + +// 获取单个任务详情 +export function fetchContactImportTaskDetail(id: number) { + return request("/v1/workbench/detail", { id }, "GET"); +} + +// 创建通讯录导入任务 +export function createContactImportTask(data: Allocation): Promise { + return request("/v1/workbench/create", { ...data, type: 6 }, "POST"); +} + +// 更新通讯录导入任务 +export function updateContactImportTask(data: Allocation): Promise { + return request("/v1/workbench/update", { ...data, type: 6 }, "POST"); +} + +// 删除通讯录导入任务 +export function deleteContactImportTask(id: number): Promise { + return request("/v1/workbench/delete", { id }, "DELETE"); +} + +// 切换任务状态 +export function toggleContactImportTask(data: { + id: number; + status: number; +}): Promise { + return request("/v1/workbench/update-status", { ...data }, "POST"); +} + +// 复制通讯录导入任务 +export function copyContactImportTask(id: number): Promise { + return request("/v1/workbench/copy", { id }, "POST"); +} + +// 获取导入记录 +export function fetchImportRecords( + workbenchId: number, + page: number = 1, + limit: number = 20, + keyword?: string, +): Promise> { + return request( + "/v1/workbench/import-records", + { + workbenchId, + page, + limit, + keyword, + }, + "GET", + ); +} + +// 获取统计数据 +export function fetchImportStats(): Promise { + return request("/v1/workbench/import-stats", {}, "GET"); +} + +// 获取设备组列表 +export function fetchDeviceGroups(): Promise { + return request("/v1/device/groups", {}, "GET"); +} + +// 手动触发导入 +export function triggerImport(taskId: number): Promise { + return request("/v1/workbench/trigger-import", { taskId }, "POST"); +} + +// 批量操作任务 +export function batchOperateTasks(data: { + taskIds: number[]; + operation: "start" | "stop" | "delete"; +}): Promise { + return request("/v1/workbench/batch-operate", data, "POST"); +} diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/form/data.ts b/Cunkebao/src/pages/mobile/workspace/contact-import/form/data.ts new file mode 100644 index 00000000..4847665e --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/form/data.ts @@ -0,0 +1,168 @@ +// 分配接口类型 +export interface Allocation { + /** 主键ID */ + id?: number; + /** 任务名称 */ + name: string; + //是否启用0关闭,1启用 + status: number; + //任务类型,固定为6 + type: number; + /** 工作台ID */ + workbenchId: number; + + /** 设备id */ + deviceGroups: number[]; + /** 流量池 */ + pools?: JSON | null; + + /** 分配数量 */ + num?: number | null; + + /** 是否清除现有联系人,默认0 */ + clearContact?: number; + + /** 备注类型 0不备注 1年月日 2月日 3自定义,默认0 */ + remarkType: number; + + /** 备注 */ + remark?: string | null; + + /** 开始时间 */ + startTime?: string | null; + + /** 结束时间 */ + endTime?: string | null; + [key: string]: any; +} +// 通讯录导入任务状态 +export type ContactImportTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 + +// 设备组信息 +export interface DeviceGroup { + id: string; + name: string; + deviceCount: number; + status: "online" | "offline"; + lastActive: string; +} + +// 通讯录导入记录 +export interface ContactImportRecord { + id: string; + workbenchId: string; + wechatAccountId: string; + deviceId: string; + num: number; + clientId: string; + remarkType: string; + remarkValue: string; + startTime: string; + endTime: string; + createTime: string; + operatorName: string; + operatorAvatar: string; + deviceName: string; + importStatus: "success" | "failed" | "pending"; + errorMessage?: string; +} + +// 通讯录导入任务配置 +export interface ContactImportTaskConfig { + id: number; + workbenchId: number; + devices: number[]; + pools: number[]; + num: number; + clearContact: number; + remarkType: number; + remark: string; + startTime: string; + endTime: string; + createTime: string; +} + +// 通讯录导入任务 +export interface ContactImportTask { + id: number; + companyId: number; + name: string; + type: number; + status: ContactImportTaskStatus; + autoStart: number; + userId: number; + createTime: string; + updateTime: string; + config: ContactImportTaskConfig; + creatorName: string; + auto_like: any; + moments_sync: any; + traffic_config: any; + group_push: any; + group_create: any; + // 计算属性,用于向后兼容 + deviceGroups?: string[]; + todayImportCount?: number; + totalImportCount?: number; + maxImportsPerDay?: number; + importInterval?: number; +} + +// 创建通讯录导入任务数据 +export interface CreateContactImportTaskData { + name: string; + type: number; + config: { + devices: number[]; + pools: number[]; + num: number; + clearContact: number; + remarkType: number; + remark: string; + startTime: string; + endTime: string; + }; +} + +// 更新通讯录导入任务数据 +export interface UpdateContactImportTaskData + extends CreateContactImportTaskData { + id: number; +} + +// 任务配置 +export interface TaskConfig { + deviceGroups: string[]; + num: number; + clientId: string; + remarkType: string; + remarkValue: string; + startTime: string; + endTime: string; + maxImportsPerDay: number; + importInterval: number; +} + +// API响应 +export interface ApiResponse { + code: number; + msg: string; + data: T; +} + +// 分页响应 +export interface PaginatedResponse { + list: T[]; + total: number; + page: number; + limit: number; +} + +// 统计数据 +export interface ImportStats { + totalTasks: number; + activeTasks: number; + todayImports: number; + totalImports: number; + successRate: number; +} diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.module.scss b/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.module.scss new file mode 100644 index 00000000..bdc76bee --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.module.scss @@ -0,0 +1,146 @@ +.basicSection { + background: none; + border-radius: 0; + box-shadow: none; + padding: 24px 16px 0 16px; + width: 100%; + max-width: 600px; + margin: 0 auto; +} + +.formItem { + margin-bottom: 24px; +} + +.formLabel { + font-size: 15px; + color: #222; + font-weight: 500; + margin-bottom: 10px; + display: block; +} + +.input { + height: 44px; + border-radius: 8px; + font-size: 15px; + width: 100%; +} + +.select { + width: 100%; + height: 44px; + + :global(.ant-select-selector) { + height: 44px !important; + border-radius: 8px !important; + font-size: 15px !important; + } + + :global(.ant-select-selection-item) { + line-height: 42px !important; + } +} + +.timePicker { + width: 100%; + height: 44px; + + :global(.ant-picker) { + width: 100%; + height: 44px; + border-radius: 8px; + font-size: 15px; + } +} + +.counterRow { + display: flex; + align-items: center; + gap: 0; +} + +.stepperContainer { + display: flex; + align-items: center; + gap: 0; +} + +.stepperButton { + width: 40px; + height: 40px; + border-radius: 8px; + background: #fff; + border: 1px solid #e5e7eb; + font-size: 16px; + color: #188eee; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border 0.2s; + + &:hover { + border: 1px solid #188eee; + } +} + +.stepperInput { + width: 80px; + height: 40px; + border-radius: 0; + border: 1px solid #e5e7eb; + border-left: none; + border-right: none; + text-align: center; + font-size: 16px; + font-weight: 600; + color: #222; + padding: 0 8px; +} + +.counterTip { + font-size: 12px; + color: #aaa; + margin-top: 4px; +} + +.buttonGroup { + display: flex; + gap: 12px; + padding: 14px; + background: #fff; +} + +.submitButton { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 120px; + flex: 1; +} + +.resetButton { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 100px; +} + +.deviceSelection { + width: 100%; + + :global { + .ant-input { + border-radius: 8px; + border: 1px solid #e8e8e8; + padding: 12px 16px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } + } +} diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.tsx b/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.tsx new file mode 100644 index 00000000..a7606240 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.tsx @@ -0,0 +1,355 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { PlusOutlined, MinusOutlined } from "@ant-design/icons"; +import { Button, Input, message, TimePicker, Select, Switch } from "antd"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import DeviceSelection from "@/components/DeviceSelection"; +import { + createContactImportTask, + updateContactImportTask, + fetchContactImportTaskDetail, +} from "./api"; +import { Allocation } from "./data"; + +import style from "./index.module.scss"; +import dayjs from "dayjs"; + +const ContactImportForm: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id?: string }>(); + const isEdit = !!id; + + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + name: "", // 任务名称 + status: 1, // 是否启用,默认启用 + type: 6, // 任务类型,固定为6 + workbenchId: 1, // 默认工作台ID + deviceGroups: [] as number[], + pools: [] as any[], + num: 50, + clearContact: 0, + remarkType: 0, + remark: "", + startTime: dayjs("09:00", "HH:mm"), + endTime: dayjs("21:00", "HH:mm"), + // 保留原有字段用于UI显示 + deviceGroupsOptions: [] as any[], + }); + + // 处理设备选择 + const handleDeviceSelect = (selectedDevices: any[]) => { + setFormData(prev => ({ + ...prev, + deviceGroupsOptions: selectedDevices, + deviceGroups: selectedDevices.map(device => device.id), // 提取设备ID存储到deviceGroups数组 + })); + }; + + // 获取任务详情(编辑模式) + const loadTaskDetail = async () => { + if (!id) return; + + try { + setLoading(true); + const data = await fetchContactImportTaskDetail(Number(id)); + if (data) { + const config = data.config || {}; + + // 构造设备选择组件需要的数据格式 + const deviceGroupsOptions = config.deviceGroupsOptions || []; + + setFormData({ + name: data.name || "", + status: data.status || 1, + type: data.type || 6, + workbenchId: config.workbenchId || 1, + deviceGroups: + deviceGroupsOptions.map((device: any) => device.id) || [], + pools: config.pools ? JSON.parse(JSON.stringify(config.pools)) : [], + num: config.num || 50, + clearContact: config.clearContact || 0, + remarkType: config.remarkType || 0, + remark: config.remark || "", + startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null, + endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null, + deviceGroupsOptions, + }); + } + } catch (error) { + console.error("Failed to load task detail:", error); + message.error("获取任务详情失败"); + navigate("/workspace/contact-import/list"); + } finally { + setLoading(false); + } + }; + + // 更新表单数据 + const handleUpdateFormData = (data: Partial) => { + setFormData(prev => ({ ...prev, ...data })); + }; + + // 提交表单 + const handleSubmit = async () => { + // 表单验证 + if (!formData.name.trim()) { + message.error("请输入任务名称"); + return; + } + + if (formData.deviceGroups.length === 0) { + message.error("请选择设备"); + return; + } + + if (!formData.num || formData.num <= 0) { + message.error("请输入有效的分配数量"); + return; + } + + // 验证开始时间不得大于结束时间 + if (formData.startTime && formData.endTime) { + if (formData.startTime.isAfter(formData.endTime)) { + message.error("开始时间不得大于结束时间"); + return; + } + } + + setLoading(true); + try { + const submitData: Partial = { + name: formData.name, + status: formData.status, + type: formData.type, + workbenchId: formData.workbenchId, + deviceGroups: formData.deviceGroups, + pools: JSON.parse(JSON.stringify(formData.pools)), + num: formData.num, + clearContact: formData.clearContact, + remarkType: formData.remarkType, + remark: formData.remark || null, + startTime: formData.startTime?.format("HH:mm") || null, + endTime: formData.endTime?.format("HH:mm") || null, + }; + + if (isEdit && id) { + await updateContactImportTask({ + ...submitData, + id: Number(id), + } as Allocation); + message.success("更新成功"); + } else { + await createContactImportTask(submitData as Allocation); + message.success("创建成功"); + } + + navigate("/workspace/contact-import/list"); + } catch (error) { + message.error(isEdit ? "更新失败" : "创建失败"); + } finally { + setLoading(false); + } + }; + + // 重置表单 + const handleReset = () => { + setFormData({ + name: "", + status: 1, + type: 6, + workbenchId: 1, + deviceGroups: [], + pools: [], + num: 50, + clearContact: 0, + remarkType: 0, + remark: "", + startTime: dayjs("09:00", "HH:mm"), + endTime: dayjs("21:00", "HH:mm"), + deviceGroupsOptions: [], + }); + }; + + useEffect(() => { + if (isEdit) { + loadTaskDetail(); + } + }, [id, isEdit]); + + return ( + } + footer={ +
+ + +
+ } + loading={loading} + > +
+
+
+
任务名称
+ handleUpdateFormData({ name: e.target.value })} + className={style.input} + /> +
为此导入任务设置一个名称
+
+ +
+
设备选择
+ +
选择要分配联系人的设备
+
+ +
+
分配数量
+
+
+
要分配给设备的联系人数量
+
+ +
+
清除现有联系人
+ + handleUpdateFormData({ clearContact: checked ? 1 : 0 }) + } + /> +
是否清除设备上现有的联系人
+
+ +
+
备注类型
+ +
选择联系人备注的格式
+
+ + {formData.remarkType === 3 && ( +
+
自定义备注
+ handleUpdateFormData({ remark: e.target.value })} + className={style.input} + /> +
输入自定义的备注内容
+
+ )} + +
+
开始时间
+ + handleUpdateFormData({ + startTime: time, + }) + } + format="HH:mm" + placeholder="请选择开始时间" + className={style.timePicker} + /> +
设置每天开始导入的时间
+
+ +
+
结束时间
+ + handleUpdateFormData({ + endTime: time, + }) + } + format="HH:mm" + placeholder="请选择结束时间" + className={style.timePicker} + /> +
设置每天结束导入的时间
+
+ +
+ 是否启用 + + handleUpdateFormData({ status: check ? 1 : 0 }) + } + /> +
+
+
+
+ ); +}; + +export default ContactImportForm; diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/list/api.ts b/Cunkebao/src/pages/mobile/workspace/contact-import/list/api.ts new file mode 100644 index 00000000..ad8e91a5 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/api.ts @@ -0,0 +1,97 @@ +import request from "@/api/request"; +import { + ContactImportTask, + CreateContactImportTaskData, + UpdateContactImportTaskData, + ContactImportRecord, + PaginatedResponse, + ImportStats, +} from "./data"; + +// 获取通讯录导入任务列表 +export function fetchContactImportTasks( + params = { type: 6, page: 1, limit: 10 }, +) { + return request("/v1/workbench/list", params, "GET"); +} + +// 获取单个任务详情 +export function fetchContactImportTaskDetail( + id: number, +): Promise { + return request("/v1/workbench/detail", { id }, "GET"); +} + +// 创建通讯录导入任务 +export function createContactImportTask( + data: CreateContactImportTaskData, +): Promise { + return request("/v1/workbench/create", { ...data, type: 6 }, "POST"); +} + +// 更新通讯录导入任务 +export function updateContactImportTask( + data: UpdateContactImportTaskData, +): Promise { + return request("/v1/workbench/update", { ...data, type: 6 }, "POST"); +} + +// 删除通讯录导入任务 +export function deleteContactImportTask(id: number): Promise { + return request("/v1/workbench/delete", { id }, "DELETE"); +} + +// 切换任务状态 +export function toggleContactImportTask(data: { + id: number; + status: number; +}): Promise { + return request("/v1/workbench/update-status", { ...data }, "POST"); +} + +// 复制通讯录导入任务 +export function copyContactImportTask(id: number): Promise { + return request("/v1/workbench/copy", { id }, "POST"); +} + +// 获取导入记录 +export function fetchImportRecords( + workbenchId: number, + page: number = 1, + limit: number = 20, + keyword?: string, +): Promise> { + return request( + "/v1/workbench/import-records", + { + workbenchId, + page, + limit, + keyword, + }, + "GET", + ); +} + +// 获取统计数据 +export function fetchImportStats(): Promise { + return request("/v1/workbench/import-stats", {}, "GET"); +} + +// 获取设备组列表 +export function fetchDeviceGroups(): Promise { + return request("/v1/device/groups", {}, "GET"); +} + +// 手动触发导入 +export function triggerImport(taskId: number): Promise { + return request("/v1/workbench/trigger-import", { taskId }, "POST"); +} + +// 批量操作任务 +export function batchOperateTasks(data: { + taskIds: number[]; + operation: "start" | "stop" | "delete"; +}): Promise { + return request("/v1/workbench/batch-operate", data, "POST"); +} diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/list/data.ts b/Cunkebao/src/pages/mobile/workspace/contact-import/list/data.ts new file mode 100644 index 00000000..33986379 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/data.ts @@ -0,0 +1,131 @@ +// 通讯录导入任务状态 +export type ContactImportTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 + +// 设备组信息 +export interface DeviceGroup { + id: string; + name: string; + deviceCount: number; + status: "online" | "offline"; + lastActive: string; +} + +// 通讯录导入记录 +export interface ContactImportRecord { + id: string; + workbenchId: string; + wechatAccountId: string; + deviceId: string; + num: number; + clientId: string; + remarkType: string; + remarkValue: string; + startTime: string; + endTime: string; + createTime: string; + operatorName: string; + operatorAvatar: string; + deviceName: string; + importStatus: "success" | "failed" | "pending"; + errorMessage?: string; +} + +// 通讯录导入任务配置 +export interface ContactImportTaskConfig { + id: number; + workbenchId: number; + devices: number[]; + pools: number[]; + num: number; + clearContact: number; + remarkType: number; + remark: string; + startTime: string; + endTime: string; + createTime: string; +} + +// 通讯录导入任务 +export interface ContactImportTask { + id: number; + companyId: number; + name: string; + type: number; + status: ContactImportTaskStatus; + autoStart: number; + userId: number; + createTime: string; + updateTime: string; + config: ContactImportTaskConfig; + creatorName: string; + auto_like: any; + moments_sync: any; + traffic_config: any; + group_push: any; + group_create: any; + // 计算属性,用于向后兼容 + deviceGroups?: string[]; + todayImportCount?: number; + totalImportCount?: number; + maxImportsPerDay?: number; + importInterval?: number; +} + +// 创建通讯录导入任务数据 +export interface CreateContactImportTaskData { + name: string; + type: number; + config: { + devices: number[]; + pools: number[]; + num: number; + clearContact: number; + remarkType: number; + remark: string; + startTime: string; + endTime: string; + }; +} + +// 更新通讯录导入任务数据 +export interface UpdateContactImportTaskData + extends CreateContactImportTaskData { + id: number; +} + +// 任务配置 +export interface TaskConfig { + deviceGroups: string[]; + num: number; + clientId: string; + remarkType: string; + remarkValue: string; + startTime: string; + endTime: string; + maxImportsPerDay: number; + importInterval: number; +} + +// API响应 +export interface ApiResponse { + code: number; + msg: string; + data: T; +} + +// 分页响应 +export interface PaginatedResponse { + list: T[]; + total: number; + page: number; + limit: number; +} + +// 统计数据 +export interface ImportStats { + totalTasks: number; + activeTasks: number; + todayImports: number; + totalImports: number; + successRate: number; +} diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.module.scss b/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.module.scss new file mode 100644 index 00000000..082d3233 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.module.scss @@ -0,0 +1,241 @@ +.container { + padding: 0 14px; +} + +.toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding: 12px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.searchBox { + flex: 1; + + :global(.ant-input-affix-wrapper) { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: #40a9ff; + } + + &:focus-within { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } +} + +.actions { + display: flex; + gap: 8px; + + :global(.adm-button) { + border-radius: 6px; + } +} + +.taskList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: #666; + font-size: 14px; + gap: 8px; +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.emptyIcon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.emptyText { + color: #666; + font-size: 14px; + margin-bottom: 16px; +} + +.taskCard { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + :global(.adm-card-body) { + padding: 16px; + } +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.taskInfo { + flex: 1; +} + +.taskName { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 4px; + line-height: 1.4; +} + +.taskStatus { + font-size: 12px; + font-weight: 500; + padding: 2px 8px; + border-radius: 12px; + background-color: rgba(82, 196, 26, 0.1); + display: inline-block; +} + +.cardMenu { + position: relative; +} + +.menuButton { + padding: 4px 8px; + color: #666; + + &:hover { + color: #1890ff; + background-color: #f0f8ff; + } +} + +.menuDropdown { + position: absolute; + top: 100%; + right: 0; + background: #fff; + border: 1px solid #d9d9d9; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + min-width: 100px; + overflow: hidden; +} + +.menuItem { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + font-size: 14px; + color: #333; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f5f5f5; + } + + &:last-child { + color: #ff4d4f; + + &:hover { + background-color: #fff2f0; + } + } +} + +.cardContent { + margin-bottom: 16px; +} + +.taskDetail { + display: flex; + align-items: center; + margin-bottom: 8px; + font-size: 14px; + + &:last-child { + margin-bottom: 0; + } +} + +.label { + color: #666; + margin-right: 8px; + min-width: 70px; + flex-shrink: 0; +} + +.value { + color: #333; + flex: 1; + word-break: break-all; +} + +.cardActions { + display: flex; + gap: 8px; + padding-top: 12px; + border-top: 1px solid #f0f0f0; + + :global(.adm-button) { + flex: 1; + border-radius: 6px; + font-size: 14px; + } +} + +// 响应式设计 +@media (max-width: 480px) { + .toolbar { + flex-direction: column; + align-items: stretch; + gap: 8px; + } + + .actions { + justify-content: space-between; + } + + .taskName { + font-size: 15px; + } + + .taskDetail { + font-size: 13px; + } + + .label { + min-width: 60px; + } +} diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.tsx b/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.tsx new file mode 100644 index 00000000..ef1e6661 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.tsx @@ -0,0 +1,345 @@ +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button, Toast, SpinLoading, Dialog, Card } from "antd-mobile"; +import NavCommon from "@/components/NavCommon"; +import { Input } from "antd"; +import { + PlusOutlined, + CopyOutlined, + DeleteOutlined, + SearchOutlined, + ReloadOutlined, + EyeOutlined, + EditOutlined, + MoreOutlined, + ContactsOutlined, +} from "@ant-design/icons"; + +import Layout from "@/components/Layout/Layout"; +import { + fetchContactImportTasks, + deleteContactImportTask, + toggleContactImportTask, + copyContactImportTask, +} from "./api"; +import { ContactImportTask } from "./data"; +import style from "./index.module.scss"; + +// 卡片菜单组件 +interface CardMenuProps { + onView: () => void; + onEdit: () => void; + onCopy: () => void; + onDelete: () => void; +} + +const CardMenu: React.FC = ({ + onView, + onEdit, + onCopy, + onDelete, +}) => { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + return ( +
+ + {open && ( +
+
+ 查看 +
+
+ 编辑 +
+
+ 复制 +
+
+ 删除 +
+
+ )} +
+ ); +}; + +const ContactImport: React.FC = () => { + const navigate = useNavigate(); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(false); + const [searchKeyword, setSearchKeyword] = useState(""); + const [filteredTasks, setFilteredTasks] = useState([]); + + // 获取任务列表 + const loadTasks = async () => { + setLoading(true); + try { + const response = await fetchContactImportTasks(); + const data = response?.list || []; + setTasks(data); + setFilteredTasks(data); + } catch (error) { + Toast.show({ + content: "获取任务列表失败", + icon: "fail", + }); + } finally { + setLoading(false); + } + }; + + // 搜索过滤 + const handleSearch = (keyword: string) => { + setSearchKeyword(keyword); + if (!keyword.trim()) { + setFilteredTasks(tasks); + } else { + const filtered = tasks.filter( + task => + task.name.toLowerCase().includes(keyword.toLowerCase()) || + (task.config?.remark || "") + .toLowerCase() + .includes(keyword.toLowerCase()) || + task.creatorName.toLowerCase().includes(keyword.toLowerCase()), + ); + setFilteredTasks(filtered); + } + }; + + // 删除任务 + const handleDelete = async (id: number) => { + const result = await Dialog.confirm({ + content: "确定要删除这个通讯录导入任务吗?", + }); + if (result) { + try { + await deleteContactImportTask(id); + Toast.show({ + content: "删除成功", + icon: "success", + }); + loadTasks(); + } catch (error) { + Toast.show({ + content: "删除失败", + icon: "fail", + }); + } + } + }; + + // 切换任务状态 + const handleToggleStatus = async (task: ContactImportTask) => { + try { + await toggleContactImportTask({ + id: task.id, + status: task.status === 1 ? 2 : 1, + }); + Toast.show({ + content: task.status === 1 ? "任务已暂停" : "任务已启动", + icon: "success", + }); + loadTasks(); + } catch (error) { + Toast.show({ + content: "操作失败", + icon: "fail", + }); + } + }; + + // 复制任务 + const handleCopy = async (id: number) => { + try { + await copyContactImportTask(id); + Toast.show({ + content: "复制成功", + icon: "success", + }); + loadTasks(); + } catch (error) { + Toast.show({ + content: "复制失败", + icon: "fail", + }); + } + }; + + // 查看详情 + const handleView = (id: number) => { + navigate(`/workspace/contact-import/detail/${id}`); + }; + + // 编辑任务 + const handleEdit = (id: number) => { + navigate(`/workspace/contact-import/form/${id}`); + }; + + // 格式化状态文本 + const getStatusText = (status: number) => { + return status === 1 ? "运行中" : "已暂停"; + }; + + // 格式化状态颜色 + const getStatusColor = (status: number) => { + return status === 1 ? "#52c41a" : "#faad14"; + }; + + useEffect(() => { + loadTasks(); + }, []); + + return ( + + navigate("/workspace")} + title="通讯录导入" + right={ + + } + /> + {/* 搜索栏 */} +
+
+ setSearchKeyword(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ + } + > +
+ {/* 任务列表 */} +
+ {loading ? ( +
+ 加载中... +
+ ) : filteredTasks.length === 0 ? ( +
+ +
+ {searchKeyword ? "未找到相关任务" : "暂无通讯录导入任务"} +
+ {!searchKeyword && ( + + )} +
+ ) : ( + filteredTasks.map(task => ( + +
+
+
{task.name}
+
+ {getStatusText(task.status)} +
+
+ handleView(task.id)} + onEdit={() => handleEdit(task.id)} + onCopy={() => handleCopy(task.id)} + onDelete={() => handleDelete(task.id)} + /> +
+
+
+ 备注类型: + + {task.config?.remarkType === 1 ? "自定义备注" : "其他"} + +
+
+ 设备数量: + + {task.config?.devices?.length || 0} + +
+
+ 导入数量: + {task.config?.num || 0} +
+
+ 创建时间: + {task.createTime} +
+
+
+ + +
+
+ )) + )} +
+
+
+ ); +}; + +export default ContactImport; diff --git a/Cunkebao/src/pages/mobile/workspace/main/index.tsx b/Cunkebao/src/pages/mobile/workspace/main/index.tsx index 1e4bb785..6627d630 100644 --- a/Cunkebao/src/pages/mobile/workspace/main/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/main/index.tsx @@ -7,6 +7,7 @@ import { TeamOutlined, LinkOutlined, ClockCircleOutlined, + ContactsOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; @@ -69,6 +70,17 @@ const Workspace: React.FC = () => { path: "/workspace/traffic-distribution", bgColor: "#e6f7ff", }, + { + id: "contact-import", + name: "通讯录导入", + description: "批量导入通讯录联系人", + icon: ( + + ), + path: "/workspace/contact-import/list", + bgColor: "#f9f0ff", + isNew: true, + }, ]; return ( diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx index 984698a1..b6d8d467 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx @@ -33,7 +33,7 @@ const defaultForm = { syncType: 1, // 1=业务号 2=人设号 accountType: "business" as "business" | "personal", // 仅UI用 enabled: true, - deveiceGroups: [] as any[], + deviceGroups: [] as any[], contentGroups: [] as any[], // 存完整内容库对象数组 contentTypes: ["text", "image", "video"], targetTags: [] as string[], @@ -47,7 +47,7 @@ const NewMomentsSync: React.FC = () => { const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); const [formData, setFormData] = useState({ ...defaultForm }); - const [deveiceGroupsOptions, setSelectedDevicesOptions] = useState< + const [deviceGroupsOptions, setSelectedDevicesOptions] = useState< DeviceSelectionItem[] >([]); const [contentGroupsOptions, setContentGroupsOptions] = useState< @@ -70,14 +70,14 @@ const NewMomentsSync: React.FC = () => { syncType: res.accountType === 1 ? 1 : 2, accountType: res.accountType === 1 ? "business" : "personal", enabled: res.status === 1, - deveiceGroups: res.config?.deveiceGroups || [], + deviceGroups: res.config?.deviceGroups || [], // 关键:用id字符串数组回填 contentGroups: res.config?.contentGroups || [], // 直接用对象数组 contentTypes: res.config?.contentTypes || ["text", "image", "video"], targetTags: res.config?.targetTags || [], filterKeywords: res.config?.filterKeywords || [], }); - setSelectedDevicesOptions(res.config?.deveiceGroupsOptions || []); + setSelectedDevicesOptions(res.config?.deviceGroupsOptions || []); setContentGroupsOptions(res.config?.contentGroupsOptions || []); } } catch { @@ -110,7 +110,7 @@ const NewMomentsSync: React.FC = () => { }; const handleDevicesChange = (devices: DeviceSelectionItem[]) => { setSelectedDevicesOptions(devices); - updateForm({ deveiceGroups: devices.map(d => d.id) }); + updateForm({ deviceGroups: devices.map(d => d.id) }); }; const handleContentChange = (libs: ContentItem[]) => { setContentGroupsOptions(libs); @@ -122,7 +122,7 @@ const NewMomentsSync: React.FC = () => { message.error("请输入任务名称"); return; } - if (formData.deveiceGroups.length === 0) { + if (formData.deviceGroups.length === 0) { message.error("请选择设备"); return; } @@ -134,7 +134,7 @@ const NewMomentsSync: React.FC = () => { try { const params = { name: formData.taskName, - deveiceGroups: formData.deveiceGroups, + deviceGroups: formData.deviceGroups, contentGroups: formData.contentGroups.map((lib: any) => lib.id), syncInterval: formData.syncInterval, syncCount: formData.syncCount, @@ -260,7 +260,7 @@ const NewMomentsSync: React.FC = () => {
选择设备
= ({ setLoading(true); try { const detailRes = await fetchDistributionRuleDetail(ruleId); - const deviceData = detailRes?.config?.deveiceGroupsOptions || []; + const deviceData = detailRes?.config?.deviceGroupsOptions || []; setDevices(deviceData); } catch (error) { console.error("获取设备详情失败:", error); diff --git a/Cunkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss b/Cunkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss index 75333cfb..6bf58b79 100644 --- a/Cunkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss +++ b/Cunkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss @@ -43,33 +43,91 @@ .headerRight { display: flex; align-items: center; + gap: 16px; } .userInfo { - cursor: pointer; - padding: 8px 12px; - border-radius: 6px; - transition: all 0.3s; + display: flex; + align-items: center; + gap: 16px; + padding: 8px 0; - &:hover { - background-color: #f5f5f5; - } .suanli { - font-size: 16px; + display: flex; + align-items: center; + gap: 4px; + font-size: 14px; color: #666; + font-weight: 500; + padding: 6px 12px; + background: #f8f9fa; + border-radius: 20px; + border: 1px solid #e9ecef; + .suanliIcon { - font-size: 24px; + font-size: 16px; + color: #ffc107; } } } -.avatar { - border: 2px solid #f0f0f0; +.messageButton { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 50%; transition: all 0.3s; + border: 1px solid #e9ecef; + background: #fff; &:hover { + background-color: #f8f9fa; border-color: #1890ff; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15); } + + .anticon { + font-size: 18px; + color: #666; + } +} + +.userSection { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 8px 16px; + border-radius: 24px; + transition: all 0.3s; + border: 1px solid #e9ecef; + background: #fff; + + &:hover { + background-color: #f8f9fa; + border-color: #1890ff; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15); + } +} + +.userNickname { + font-size: 14px; + color: #333; + font-weight: 600; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.avatar { + border: 2px solid #e9ecef; + transition: all 0.3s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .username { diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/api.ts b/Cunkebao/src/pages/pc/ckbox/weChat/api.ts index 98bc336a..af476f26 100644 --- a/Cunkebao/src/pages/pc/ckbox/weChat/api.ts +++ b/Cunkebao/src/pages/pc/ckbox/weChat/api.ts @@ -13,7 +13,48 @@ import { //读取聊天信息 //kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount +function jsonToQueryString(json) { + const params = new URLSearchParams(); + for (const key in json) { + if (Object.prototype.hasOwnProperty.call(json, key)) { + params.append(key, json[key]); + } + } + return params.toString(); +} +//转移客户 +export function WechatFriendAllot(params: { + wechatFriendId?: number; + wechatChatroomId?: number; + toAccountId: number; + notifyReceiver: boolean; + comment: string; +}) { + return request( + "/api/wechatFriend/allot?" + jsonToQueryString(params), + undefined, + "PUT", + ); +} +//获取可转移客服列表 +export function getTransferableAgentList() { + return request("/api/account/myDepartmentAccountsForTransfer", {}, "GET"); +} + +// 微信好友列表 +export function WechatFriendRebackAllot(params: { + wechatFriendId?: number; + wechatChatroomId?: number; +}) { + return request( + "/api/wechatFriend/rebackAllot?" + jsonToQueryString(params), + undefined, + "PUT", + ); +} + +// 微信群列表 export function WechatGroup(params) { return request("/api/WechatGroup/list", params, "GET"); } diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/chatRecord/index.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/chatRecord/index.tsx new file mode 100644 index 00000000..3068e42d --- /dev/null +++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/chatRecord/index.tsx @@ -0,0 +1,150 @@ +import React, { useState } from "react"; +import { Button, Modal, Input, DatePicker, message } from "antd"; +import { MessageOutlined } from "@ant-design/icons"; +import dayjs from "dayjs"; +import { useWeChatStore } from "@/store/module/weChat/weChat"; + +const { RangePicker } = DatePicker; + +interface ChatRecordProps { + className?: string; + disabled?: boolean; +} + +const ChatRecord: React.FC = ({ + className, + disabled = false, +}) => { + const [visible, setVisible] = useState(false); + const [searchContent, setSearchContent] = useState(""); + const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>( + null, + ); + const [loading, setLoading] = useState(false); + const SearchMessage = useWeChatStore(state => state.SearchMessage); + + // 打开弹窗 + const openModal = () => { + setVisible(true); + }; + + // 关闭弹窗并重置状态 + const closeModal = () => { + setVisible(false); + setSearchContent(""); + setDateRange(null); + setLoading(false); + }; + + // 执行查找 + const handleSearch = async () => { + if (!dateRange) { + message.warning("请选择时间范围"); + return; + } + + try { + setLoading(true); + const [From, To] = dateRange; + const searchData = { + From: From.unix() * 1000, + To: To.unix() * 1000, + keyword: searchContent.trim(), + }; + await SearchMessage(searchData); + + message.success("查找完成"); + closeModal(); + } catch (error) { + console.error("查找失败:", error); + message.error("查找失败,请重试"); + } finally { + setLoading(false); + } + }; + + return ( + <> +
+ + 聊天记录 +
+ + + + +
, + ]} + > +
+ {/* 时间范围选择 */} +
+
+ 时间范围 +
+ +
+ + {/* 查找内容输入 */} +
+
+ 查找内容 +
+ setSearchContent(e.target.value)} + size="large" + maxLength={100} + showCount + disabled={loading} + /> +
+
+ + + ); +}; + +export default ChatRecord; diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx new file mode 100644 index 00000000..be2f9cb2 --- /dev/null +++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx @@ -0,0 +1,253 @@ +import React, { useState } from "react"; +import { Button, Modal, Select, Input, message } from "antd"; +import { ShareAltOutlined } from "@ant-design/icons"; +import { + getTransferableAgentList, + WechatFriendAllot, + WechatFriendRebackAllot, +} from "@/pages/pc/ckbox/weChat/api"; +import { useCurrentContact } from "@/store/module/weChat/weChat"; +import { useCkChatStore } from "@/store/module/ckchat/ckchat"; +import { contractService, weChatGroupService } from "@/utils/db"; +const { TextArea } = Input; +const { Option } = Select; + +interface ToContractProps { + className?: string; + disabled?: boolean; +} +interface DepartItem { + id: number; + userName: string; + realName: string; + nickname: string; + avatar: string; + memo: string; + departmentId: number; + alive: boolean; +} + +const ToContract: React.FC = ({ + className, + disabled = false, +}) => { + const currentContact = useCurrentContact(); + const [visible, setVisible] = useState(false); + const [selectedTarget, setSelectedTarget] = useState(null); + const [comment, setComment] = useState(""); + const [loading, setLoading] = useState(false); + const [customerServiceList, setCustomerServiceList] = useState( + [], + ); + const deleteChatSession = useCkChatStore(state => state.deleteChatSession); + // 打开弹窗 + const openModal = () => { + setVisible(true); + getTransferableAgentList().then(data => { + setCustomerServiceList(data); + }); + }; + + // 关闭弹窗并重置状态 + const closeModal = () => { + setVisible(false); + setSelectedTarget(null); + setComment(""); + setLoading(false); + }; + + // 确定转给他人 + const handleConfirm = async () => { + if (!selectedTarget) { + message.warning("请选择目标客服"); + return; + } + + try { + setLoading(true); + + console.log(currentContact); + + // 调用转接接口 + if (currentContact) { + if ("chatroomId" in currentContact && currentContact.chatroomId) { + await WechatFriendAllot({ + wechatChatroomId: currentContact.id, + toAccountId: selectedTarget as number, + notifyReceiver: true, + comment: comment.trim(), + }); + } else { + await WechatFriendAllot({ + wechatFriendId: currentContact.id, + toAccountId: selectedTarget as number, + notifyReceiver: true, + comment: comment.trim(), + }); + } + } + + message.success("转接成功"); + try { + // 删除聊天会话 + deleteChatSession(currentContact.id); + // 删除本地数据库记录 + if ("chatroomId" in currentContact) { + await weChatGroupService.delete(currentContact.id); + } else { + await contractService.delete(currentContact.id); + } + } catch (deleteError) { + console.error("删除本地数据失败:", deleteError); + } + closeModal(); + } catch (error) { + console.error("转接失败:", error); + message.error("转接失败,请重试"); + } finally { + setLoading(false); + } + }; + + // 一键转回 + const handleReturn = async () => { + try { + setLoading(true); + + // 调用转回接口 + if (currentContact) { + if ("chatroomId" in currentContact && currentContact.chatroomId) { + await WechatFriendRebackAllot({ + wechatChatroomId: currentContact.id, + }); + } else { + await WechatFriendRebackAllot({ + wechatFriendId: currentContact.id, + }); + } + } + + message.success("转回成功"); + try { + // 删除聊天会话 + deleteChatSession(currentContact.id); + // 删除本地数据库记录 + if ("chatroomId" in currentContact) { + await weChatGroupService.delete(currentContact.id); + } else { + await contractService.delete(currentContact.id); + } + } catch (deleteError) { + console.error("删除本地数据失败:", deleteError); + } + closeModal(); + } catch (error) { + console.error("转回失败:", error); + message.error("转回失败,请重试"); + } finally { + setLoading(false); + } + }; + + return ( + <> +
+ + 转给他人 +
+ + + +
+ + +
+
, + ]} + > +
+ {/* 目标客服选择 */} +
+
+ 目标客服 +
+ +
+ + {/* 附言输入 */} +
+
+ 附言 +
+