diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/detail/index.module.scss b/Cunkebao/src/pages/mobile/workspace/contact-import/detail/index.module.scss new file mode 100644 index 00000000..aaf11375 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/detail/index.module.scss @@ -0,0 +1,332 @@ +.container { + padding: 12px; + background-color: #f5f5f5; + min-height: 100vh; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: #666; + font-size: 14px; + gap: 8px; +} + +.empty { + display: flex; + align-items: center; + justify-content: center; + padding: 60px 20px; +} + +.actionCard { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + margin-bottom: 16px; + + :global(.adm-card-body) { + padding: 16px; + } +} + +.taskHeader { + margin-bottom: 16px; +} + +.taskInfo { + display: flex; + align-items: center; + justify-content: space-between; +} + +.taskName { + font-size: 18px; + font-weight: 600; + color: #333; + flex: 1; + margin-right: 12px; +} + +.taskStatus { + font-size: 14px; + font-weight: 500; + padding: 4px 12px; + border-radius: 12px; + background-color: rgba(82, 196, 26, 0.1); + white-space: nowrap; +} + +.actions { + display: flex; + gap: 8px; + + :global(.adm-button) { + flex: 1; + border-radius: 6px; + font-size: 14px; + } +} + +.tabs { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + overflow: hidden; + + :global(.adm-tabs-header) { + background: #fafafa; + border-bottom: 1px solid #f0f0f0; + } + + :global(.adm-tabs-tab) { + font-size: 15px; + font-weight: 500; + + &.adm-tabs-tab-active { + color: #1890ff; + } + } + + :global(.adm-tabs-tab-line) { + background: #1890ff; + } +} + +.tabContent { + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.infoCard { + background: #fff; + border-radius: 8px; + border: 1px solid #f0f0f0; + + :global(.adm-card-body) { + padding: 16px; + } +} + +.cardTitle { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.infoList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.infoItem { + display: flex; + align-items: center; + font-size: 14px; + line-height: 1.5; +} + +.label { + color: #666; + margin-right: 12px; + min-width: 80px; + flex-shrink: 0; +} + +.value { + color: #333; + flex: 1; + word-break: break-all; +} + +.recordList { + :global(.adm-list-body) { + border: none; + } +} + +.recordItem { + background: #fff; + border-radius: 8px; + border: 1px solid #f0f0f0; + margin-bottom: 12px; + + :global(.adm-list-item-content) { + padding: 16px; + } + + &:last-child { + margin-bottom: 0; + } +} + +.recordHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.recordInfo { + flex: 1; +} + +.recordDevice { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.recordTime { + font-size: 12px; + color: #999; + white-space: nowrap; +} + +.recordContent { + display: flex; + flex-direction: column; + gap: 8px; +} + +.recordDetail { + display: flex; + align-items: center; + font-size: 13px; + + .label { + min-width: 70px; + color: #666; + } + + .value { + color: #333; + } +} + +.recordError { + font-size: 13px; + color: #ff4d4f; + background: #fff2f0; + padding: 8px 12px; + border-radius: 6px; + border: 1px solid #ffccc7; +} + +.loadingMore { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: #666; + font-size: 14px; + gap: 8px; +} + +// 状态标签样式覆盖 +:global { + .adm-tag { + border-radius: 12px; + font-size: 12px; + padding: 2px 8px; + + &.adm-tag-success { + background: #f6ffed; + border-color: #b7eb8f; + color: #52c41a; + } + + &.adm-tag-danger { + background: #fff2f0; + border-color: #ffccc7; + color: #ff4d4f; + } + + &.adm-tag-warning { + background: #fff7e6; + border-color: #ffd591; + color: #fa8c16; + } + } +} + +// 下拉刷新样式 +:global(.adm-pull-to-refresh) { + .adm-pull-to-refresh-indicator { + color: #1890ff; + } +} + +// 响应式设计 +@media (max-width: 480px) { + .container { + padding: 8px; + } + + .taskName { + font-size: 16px; + } + + .taskStatus { + font-size: 13px; + padding: 3px 10px; + } + + .cardTitle { + font-size: 15px; + } + + .infoItem { + font-size: 13px; + } + + .label { + min-width: 70px; + } + + .recordDevice { + font-size: 13px; + } + + .recordTime { + font-size: 11px; + } + + .recordDetail { + font-size: 12px; + + .label { + min-width: 60px; + } + } + + .recordError { + font-size: 12px; + padding: 6px 10px; + } +} + +// 空状态样式 +:global(.adm-empty) { + padding: 40px 20px; + + .adm-empty-image { + width: 60px; + height: 60px; + opacity: 0.3; + } + + .adm-empty-description { + color: #999; + font-size: 14px; + margin-top: 12px; + } +} \ No newline at end of file diff --git a/Cunkebao/src/pages/mobile/workspace/contact-import/detail/index.tsx b/Cunkebao/src/pages/mobile/workspace/contact-import/detail/index.tsx new file mode 100644 index 00000000..06ad0310 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/detail/index.tsx @@ -0,0 +1,464 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Button, + Toast, + Card, + Tabs, + List, + Tag, + Space, + InfiniteScroll, + PullToRefresh, + Empty, + SpinLoading, +} from "antd-mobile"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { + fetchContactImportTaskDetail, + fetchImportRecords, + triggerImport, + toggleContactImportTask, +} from "../list/api"; +import { + ContactImportTask, + ContactImportRecord, +} from "../list/data"; +import { + PlayCircleOutlined, + PauseCircleOutlined, + EditOutlined, + ReloadOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ClockCircleOutlined, +} from "@ant-design/icons"; +import style from "./index.module.scss"; + +const ContactImportDetail: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + + const [task, setTask] = useState(null); + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(false); + const [recordsLoading, setRecordsLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + const [activeTab, setActiveTab] = useState("info"); + + // 获取任务详情 + const loadTaskDetail = async () => { + if (!id) return; + + setLoading(true); + try { + const response = await fetchContactImportTaskDetail(id); + const data = response?.data || response; + setTask(data); + } catch (error) { + Toast.show({ + content: "获取任务详情失败", + icon: "fail", + }); + navigate("/workspace/contact-import/list"); + } finally { + setLoading(false); + } + }; + + // 获取导入记录 + const loadRecords = async (pageNum: number = 1, reset: boolean = false) => { + if (!id) return; + + setRecordsLoading(true); + try { + const response = await fetchImportRecords(id, pageNum, 20); + const data = response?.data || response; + if (reset) { + setRecords(data.list || []); + } else { + setRecords(prev => [...prev, ...(data.list || [])]); + } + setHasMore(data.list.length === 20); + setPage(pageNum); + } catch (error) { + Toast.show({ + content: "获取记录失败", + icon: "fail", + }); + } finally { + setRecordsLoading(false); + } + }; + + // 切换任务状态 + const handleToggleStatus = async () => { + if (!task) return; + + try { + await toggleContactImportTask({ + id: task.id, + status: task.status === 1 ? 2 : 1, + }); + Toast.show({ + content: task.status === 1 ? "任务已暂停" : "任务已启动", + icon: "success", + }); + loadTaskDetail(); + } catch (error) { + Toast.show({ + content: "操作失败", + icon: "fail", + }); + } + }; + + // 手动触发导入 + const handleTriggerImport = async () => { + if (!task) return; + + try { + await triggerImport(task.id); + Toast.show({ + content: "导入任务已触发", + icon: "success", + }); + setTimeout(() => { + loadTaskDetail(); + loadRecords(1, true); + }, 1000); + } catch (error) { + Toast.show({ + content: "触发失败", + icon: "fail", + }); + } + }; + + // 刷新数据 + const handleRefresh = async () => { + await loadTaskDetail(); + if (activeTab === "records") { + await loadRecords(1, true); + } + }; + + // 加载更多记录 + const loadMoreRecords = async () => { + await loadRecords(page + 1); + }; + + // 获取状态文本和颜色 + const getStatusInfo = (status: number) => { + return status === 1 + ? { text: "运行中", color: "#52c41a" } + : { text: "已暂停", color: "#faad14" }; + }; + + // 获取记录状态图标 + const getRecordStatusIcon = (status: string) => { + switch (status) { + case "success": + return ; + case "failed": + return ; + case "pending": + return ; + default: + return null; + } + }; + + // 获取记录状态标签 + const getRecordStatusTag = (status: string) => { + switch (status) { + case "success": + return 成功; + case "failed": + return 失败; + case "pending": + return 进行中; + default: + return 未知; + } + }; + + useEffect(() => { + loadTaskDetail(); + }, [id]); + + useEffect(() => { + if (activeTab === "records" && records.length === 0) { + loadRecords(1, true); + } + }, [activeTab]); + + if (loading) { + return ( + navigate("/workspace/contact-import/list")} + > + 返回 + + } + title="任务详情" + /> + } + > +
+ 加载中... +
+
+ ); + } + + if (!task) { + return ( + navigate("/workspace/contact-import/list")} + > + 返回 + + } + title="任务详情" + /> + } + > +
+ +
+
+ ); + } + + const statusInfo = getStatusInfo(task.status); + + return ( + navigate("/workspace/contact-import/list")} + > + 返回 + + } + title="任务详情" + right={ + + } + /> + } + > + +
+ {/* 任务操作栏 */} + +
+
+
{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; \ No newline at end of file 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..962aedcc --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.module.scss @@ -0,0 +1,229 @@ +.container { + padding: 12px; + background-color: #f5f5f5; + min-height: 100vh; +} + +.form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.section { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + + :global(.adm-card-body) { + padding: 16px; + } +} + +.sectionTitle { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.actions { + margin-top: 24px; + padding: 16px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +// Form样式覆盖 +:global { + .adm-form-item { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } + + .adm-form-item-label { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + } + + .adm-input { + border: 1px solid #d9d9d9; + border-radius: 6px; + padding: 10px 12px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + + &::placeholder { + color: #bfbfbf; + } + } + + .adm-selector { + .adm-selector-item { + border: 1px solid #d9d9d9; + border-radius: 6px; + margin: 4px; + padding: 8px 12px; + font-size: 14px; + + &.adm-selector-item-active { + border-color: #1890ff; + background-color: #e6f7ff; + color: #1890ff; + } + } + } + + .adm-stepper { + .adm-stepper-button { + border: 1px solid #d9d9d9; + border-radius: 4px; + width: 32px; + height: 32px; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } + } + + .adm-stepper-input { + border: 1px solid #d9d9d9; + border-radius: 4px; + height: 32px; + margin: 0 4px; + text-align: center; + font-size: 14px; + } + } + + .adm-picker { + .adm-picker-view-column { + font-size: 14px; + } + } + + .adm-button { + border-radius: 6px; + font-size: 16px; + font-weight: 500; + + &.adm-button-primary { + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + border: none; + + &:hover { + background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%); + } + } + } +} + +// 表单验证错误样式 +:global(.adm-form-item-feedback-error) { + color: #ff4d4f; + font-size: 12px; + margin-top: 4px; +} + +:global(.adm-form-item-has-error) { + .adm-input { + border-color: #ff4d4f; + + &:focus { + border-color: #ff4d4f; + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2); + } + } + + .adm-selector-item { + border-color: #ff4d4f; + } + + .adm-stepper-button, + .adm-stepper-input { + border-color: #ff4d4f; + } +} + +// 响应式设计 +@media (max-width: 480px) { + .container { + padding: 8px; + } + + .sectionTitle { + font-size: 15px; + } + + .actions { + margin-top: 16px; + padding: 12px; + } + + :global { + .adm-form-item-label { + font-size: 13px; + } + + .adm-input { + padding: 8px 10px; + font-size: 13px; + } + + .adm-selector-item { + padding: 6px 10px; + font-size: 13px; + } + + .adm-button { + font-size: 15px; + } + } +} + +// 加载状态 +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: #666; + font-size: 14px; + gap: 8px; +} + +// 提示信息 +.tip { + background: #f6ffed; + border: 1px solid #b7eb8f; + border-radius: 6px; + padding: 12px; + margin-bottom: 16px; + font-size: 13px; + color: #52c41a; + line-height: 1.5; +} + +.warning { + background: #fff7e6; + border: 1px solid #ffd591; + border-radius: 6px; + padding: 12px; + margin-bottom: 16px; + font-size: 13px; + color: #fa8c16; + line-height: 1.5; +} \ No newline at end of file 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..cada08db --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/form/index.tsx @@ -0,0 +1,353 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Button, + Toast, + Form, + Input, + Selector, + Stepper, + DatePicker, + Card, + Space, +} from "antd-mobile"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { + createContactImportTask, + updateContactImportTask, + fetchContactImportTaskDetail, + fetchDeviceGroups, +} from "../list/api"; +import { + CreateContactImportTaskData, + UpdateContactImportTaskData, + ContactImportTask, + DeviceGroup, +} from "../list/data"; +import style from "./index.module.scss"; + +const ContactImportForm: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id?: string }>(); + const isEdit = !!id; + + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [deviceGroups, setDeviceGroups] = useState([]); + const [taskData, setTaskData] = useState(null); + const [showStartTimePicker, setShowStartTimePicker] = useState(false); + const [showEndTimePicker, setShowEndTimePicker] = useState(false); + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + + // 备注类型选项 + const remarkTypeOptions = [ + { label: "公司名称", value: "公司名称" }, + { label: "职位", value: "职位" }, + { label: "部门", value: "部门" }, + { label: "地区", value: "地区" }, + { label: "行业", value: "行业" }, + { label: "来源", value: "来源" }, + { label: "标签", value: "标签" }, + { label: "自定义", value: "自定义" }, + ]; + + // 获取设备组列表 + const loadDeviceGroups = async () => { + try { + const data = await fetchDeviceGroups(); + setDeviceGroups(data); + } catch (error) { + Toast.show({ + content: "获取设备组失败", + icon: "fail", + }); + } + }; + + // 获取任务详情(编辑模式) + const loadTaskDetail = async () => { + if (!id) return; + + try { + const data = await fetchContactImportTaskDetail(id); + if (data) { + setTaskData(data); + form.setFieldsValue({ + name: data.name, + deviceGroups: data.deviceGroups, + num: data.num, + clientId: data.clientId, + remarkType: data.remarkType, + remarkValue: data.remarkValue, + startTime: data.startTime, + endTime: data.endTime, + maxImportsPerDay: data.maxImportsPerDay, + importInterval: data.importInterval, + }); + } + } catch (error) { + Toast.show({ + content: "获取任务详情失败", + icon: "fail", + }); + navigate("/workspace/contact-import/list"); + } + }; + + // 提交表单 + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + + const taskData: CreateContactImportTaskData | UpdateContactImportTaskData = { + name: values.name, + deviceGroups: values.deviceGroups, + num: values.num, + clientId: values.clientId, + remarkType: values.remarkType, + remarkValue: values.remarkValue, + startTime: values.startTime, + endTime: values.endTime, + maxImportsPerDay: values.maxImportsPerDay, + importInterval: values.importInterval, + }; + + if (isEdit && id) { + await updateContactImportTask({ ...taskData, id }); + Toast.show({ + content: "更新成功", + icon: "success", + }); + } else { + await createContactImportTask(taskData); + Toast.show({ + content: "创建成功", + icon: "success", + }); + } + + navigate("/workspace/contact-import/list"); + } catch (error) { + Toast.show({ + content: isEdit ? "更新失败" : "创建失败", + icon: "fail", + }); + } finally { + setLoading(false); + } + }; + + // 重置表单 + const handleReset = () => { + form.resetFields(); + }; + + useEffect(() => { + loadDeviceGroups(); + if (isEdit) { + loadTaskDetail(); + } + }, [id, isEdit]); + + return ( + navigate("/workspace/contact-import/list")} + > + 返回 + + } + title={isEdit ? "编辑通讯录导入" : "新建通讯录导入"} + /> + } + > +
+
+ {/* 基本信息 */} + +
基本信息
+ + + + + + + ({ + label: `${group.name} (${group.deviceCount}台设备)`, + value: group.id, + }))} + placeholder="请选择设备组" + /> + +
+ + {/* 导入配置 */} + +
导入配置
+ + + + + + + + + + + + + + + + +
+ + {/* 时间配置 */} + +
时间配置
+ + + setShowStartTimePicker(true)} + /> + + + + setShowEndTimePicker(true)} + /> + + + setShowStartTimePicker(false)} + onConfirm={(val) => { + setStartTime(val); + form.setFieldValue('startTime', `${val.getHours().toString().padStart(2, '0')}:${val.getMinutes().toString().padStart(2, '0')}`); + setShowStartTimePicker(false); + }} + /> + + setShowEndTimePicker(false)} + onConfirm={(val) => { + setEndTime(val); + form.setFieldValue('endTime', `${val.getHours().toString().padStart(2, '0')}:${val.getMinutes().toString().padStart(2, '0')}`); + setShowEndTimePicker(false); + }} + /> + + + + + + + + +
+ + {/* 操作按钮 */} +
+ + + + +
+
+
+
+ ); +}; + +export default ContactImportForm; \ No newline at end of file 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..6c115d27 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/api.ts @@ -0,0 +1,84 @@ +import request from "@/api/request"; +import { + ContactImportTask, + CreateContactImportTaskData, + UpdateContactImportTaskData, + ContactImportRecord, + PaginatedResponse, + ImportStats, +} from "./data"; + +// 获取通讯录导入任务列表 +export function fetchContactImportTasks( + params = { type: 6, page: 1, limit: 10 }, +): Promise<{ code: number; msg: string; data: { list: ContactImportTask[] } }> { + 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"); +} \ No newline at end of file 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..c1154310 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/data.ts @@ -0,0 +1,130 @@ +// 通讯录导入任务状态 +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; +} \ No newline at end of file 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..4a52cdb6 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/contact-import/list/index.module.scss @@ -0,0 +1,247 @@ +.container { + padding: 12px; + background-color: #f5f5f5; + min-height: 100vh; +} + +.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) { + .container { + padding: 8px; + } + + .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; + } +} \ No newline at end of file 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..1551ce2d --- /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="通讯录导入" + /> + } + > +
+ {/* 搜索和操作栏 */} +
+
+ } + value={searchKeyword} + onChange={(e) => handleSearch(e.target.value)} + allowClear + /> +
+
+ + +
+
+ + {/* 任务列表 */} +
+ {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/router/module/workspace.tsx b/Cunkebao/src/router/module/workspace.tsx index 79878bf3..a52762df 100644 --- a/Cunkebao/src/router/module/workspace.tsx +++ b/Cunkebao/src/router/module/workspace.tsx @@ -15,6 +15,9 @@ import AIAssistant from "@/pages/mobile/workspace/ai-assistant/AIAssistant"; import TrafficDistribution from "@/pages/mobile/workspace/traffic-distribution/list/index"; import TrafficDistributionDetail from "@/pages/mobile/workspace/traffic-distribution/detail/index"; import NewDistribution from "@/pages/mobile/workspace/traffic-distribution/form/index"; +import ContactImportList from "@/pages/mobile/workspace/contact-import/list"; +import ContactImportForm from "@/pages/mobile/workspace/contact-import/form"; +import ContactImportDetail from "@/pages/mobile/workspace/contact-import/detail"; import PlaceholderPage from "@/components/PlaceholderPage"; import AiAnalyzer from "@/pages/mobile/workspace/ai-analyzer"; @@ -154,6 +157,27 @@ const workspaceRoutes = [ element: , auth: true, }, + // 通讯录导入 + { + path: "/workspace/contact-import/list", + element: , + auth: true, + }, + { + path: "/workspace/contact-import/form", + element: , + auth: true, + }, + { + path: "/workspace/contact-import/form/:id", + element: , + auth: true, + }, + { + path: "/workspace/contact-import/detail/:id", + element: , + auth: true, + }, ]; export default workspaceRoutes; diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index e4220016..19181f3d 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -556,12 +556,12 @@ class WorkbenchController extends Controller } break; case self::TYPE_IMPORT_CONTACT: - if (!empty($item->importContact)) { - $item->config = $item->importContact; - $item->config->devices = json_decode($item->config->devices, true); - $item->config->pools = json_decode($item->config->pools, true); + if (!empty($workbench->importContact)) { + $workbench->config = $workbench->importContact; + $workbench->config->devices = json_decode($workbench->config->devices, true); + $workbench->config->pools = json_decode($workbench->config->pools, true); } - unset($item->importContact, $item->import_contact); + unset($workbench->importContact, $workbench->import_contact); break; } unset( @@ -991,11 +991,12 @@ class WorkbenchController extends Controller } break; case self::TYPE_IMPORT_CONTACT: //联系人导入 - $config = WorkbenchImportContact::where('workbenchId',$id)->find();; + $config = WorkbenchImportContact::where('workbenchId',$id)->find(); if ($config) { $newConfig = new WorkbenchImportContact; - $newConfig->devices = json_encode($config->deveiceGroups); - $newConfig->pools = json_encode($config->pools); + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->pools = $config->pools; $newConfig->num = $config->num; $newConfig->clearContact = $config->clearContact; $newConfig->remark = $config->remark; diff --git a/Server/application/cunkebao/model/Workbench.php b/Server/application/cunkebao/model/Workbench.php index 85b967a5..edde8b74 100644 --- a/Server/application/cunkebao/model/Workbench.php +++ b/Server/application/cunkebao/model/Workbench.php @@ -64,7 +64,7 @@ class Workbench extends Model public function importContact() { - return $this->hasOne('WorkbenchTrafficConfig', 'workbenchId', 'id'); + return $this->hasOne('WorkbenchImportContact', 'workbenchId', 'id'); } diff --git a/Server/application/job/WorkbenchImportContactJob.php b/Server/application/job/WorkbenchImportContactJob.php index ec36380f..a8edfba7 100644 --- a/Server/application/job/WorkbenchImportContactJob.php +++ b/Server/application/job/WorkbenchImportContactJob.php @@ -243,7 +243,7 @@ class WorkbenchImportContactJob */ protected function getDeviceList($workbench, $config) { - $deviceIds = json_decode($config['deviceId'], true); + $deviceIds = json_decode($config['devices'], true); if (empty($deviceIds)) { return []; } @@ -313,7 +313,7 @@ class WorkbenchImportContactJob protected function getContactFromDatabase($workbench,$config) { $pools = json_decode($config['pools'], true); - $deviceIds = json_decode($config['deviceId'], true); + $deviceIds = json_decode($config['devices'], true); if (empty($pools) || empty($deviceIds)) { return false; }