diff --git a/nkebao/.env.development b/nkebao/.env.development index fe189d220..fde602957 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,5 @@ # 基础环境变量示例 -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +VITE_API_BASE_URL=http://www.yishi.com +# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/.vite/deps/_metadata.json b/nkebao/.vite/deps/_metadata.json new file mode 100644 index 000000000..b6601954e --- /dev/null +++ b/nkebao/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "efe0acf4", + "configHash": "2bed34b3", + "lockfileHash": "ef01d341", + "browserHash": "91bd3b2c", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/nkebao/.vite/deps/package.json b/nkebao/.vite/deps/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/nkebao/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/nkebao/src/api/common.ts b/nkebao/src/api/common.ts new file mode 100644 index 000000000..9ab445cce --- /dev/null +++ b/nkebao/src/api/common.ts @@ -0,0 +1,33 @@ +import request from "./request"; +/** + * 通用文件上传方法(支持图片、文件) + * @param {File} file - 要上传的文件对象 + * @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址 + * @returns {Promise} - 上传成功后返回文件url + */ +export async function uploadFile( + file: File, + uploadUrl: string = "/v1/attachment/upload" +): Promise { + try { + // 创建 FormData 对象用于文件上传 + const formData = new FormData(); + formData.append("file", file); + + // 使用 request 方法上传文件,设置正确的 Content-Type + const res = await request(uploadUrl, formData, "POST", { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + // 检查响应结果 + if (res?.code === 200 && res?.data?.url) { + return res.data.url; + } else { + throw new Error(res?.msg || "文件上传失败"); + } + } catch (e: any) { + throw new Error(e?.message || "文件上传失败"); + } +} diff --git a/nkebao/src/api/devices.ts b/nkebao/src/api/devices.ts new file mode 100644 index 000000000..b39c76830 --- /dev/null +++ b/nkebao/src/api/devices.ts @@ -0,0 +1,44 @@ +import request from "./request"; + +// 获取设备列表 +export const fetchDeviceList = (params: { + page?: number; + limit?: number; + keyword?: string; +}) => request("/v1/devices", params, "GET"); + +// 获取设备详情 +export const fetchDeviceDetail = (id: string | number) => + request(`/v1/devices/${id}`); + +// 获取设备关联微信账号 +export const fetchDeviceRelatedAccounts = (id: string | number) => + request(`/v1/wechats/related-device/${id}`); + +// 获取设备操作日志 +export const fetchDeviceHandleLogs = ( + id: string | number, + page = 1, + limit = 10 +) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET"); + +// 更新设备任务配置 +export const updateDeviceTaskConfig = (config: { + deviceId: string | number; + autoAddFriend?: boolean; + autoReply?: boolean; + momentsSync?: boolean; + aiChat?: boolean; +}) => request("/v1/devices/task-config", config, "POST"); + +// 删除设备 +export const deleteDevice = (id: number) => + request(`/v1/devices/${id}`, undefined, "DELETE"); + +// 获取设备二维码 +export const fetchDeviceQRCode = (accountId: string) => + request("/v1/api/device/add", { accountId }, "POST"); + +// 通过IMEI添加设备 +export const addDeviceByImei = (imei: string, name: string) => + request("/v1/api/device/add-by-imei", { imei, name }, "POST"); diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 2c5372bf9..b52caa9c0 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -1,22 +1,27 @@ -import axios, { AxiosInstance, AxiosRequestConfig, Method, AxiosResponse } from 'axios'; -import { Toast } from 'antd-mobile'; +import axios, { + AxiosInstance, + AxiosRequestConfig, + Method, + AxiosResponse, +} from "axios"; +import { Toast } from "antd-mobile"; const DEFAULT_DEBOUNCE_GAP = 1000; const debounceMap = new Map(); const instance: AxiosInstance = axios.create({ - baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api', + baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", timeout: 10000, headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); -instance.interceptors.request.use(config => { - const token = localStorage.getItem('token'); +instance.interceptors.request.use((config) => { + const token = localStorage.getItem("token"); if (token) { config.headers = config.headers || {}; - config.headers['Authorization'] = `Bearer ${token}`; + config.headers["Authorization"] = `Bearer ${token}`; } return config; }); @@ -27,20 +32,20 @@ instance.interceptors.response.use( if (code === 200 || success) { return res.data.data ?? res.data; } - Toast.show({ content: msg || '接口错误', position: 'top' }); + Toast.show({ content: msg || "接口错误", position: "top" }); if (code === 401) { - localStorage.removeItem('token'); + localStorage.removeItem("token"); const currentPath = window.location.pathname + window.location.search; - if (currentPath === '/login') { - window.location.href = '/login'; + if (currentPath === "/login") { + window.location.href = "/login"; } else { window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; } } - return Promise.reject(msg || '接口错误'); + return Promise.reject(msg || "接口错误"); }, - err => { - Toast.show({ content: err.message || '网络异常', position: 'top' }); + (err) => { + Toast.show({ content: err.message || "网络异常", position: "top" }); return Promise.reject(err); } ); @@ -48,17 +53,18 @@ instance.interceptors.response.use( export function request( url: string, data?: any, - method: Method = 'GET', + method: Method = "GET", config?: AxiosRequestConfig, debounceGap?: number ): Promise { - const gap = typeof debounceGap === 'number' ? debounceGap : DEFAULT_DEBOUNCE_GAP; + const gap = + typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; const key = `${method}_${url}_${JSON.stringify(data)}`; const now = Date.now(); const last = debounceMap.get(key) || 0; if (gap > 0 && now - last < gap) { - Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); - return Promise.reject('请求过于频繁,请稍后再试'); + // Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); + return Promise.reject("请求过于频繁,请稍后再试"); } debounceMap.set(key, now); @@ -67,7 +73,7 @@ export function request( method, ...config, }; - if (method.toUpperCase() === 'GET') { + if (method.toUpperCase() === "GET") { axiosConfig.params = data; } else { axiosConfig.data = data; diff --git a/nkebao/src/components/DeviceSelection/api.ts b/nkebao/src/components/DeviceSelection/api.ts new file mode 100644 index 000000000..1f28ce04e --- /dev/null +++ b/nkebao/src/components/DeviceSelection/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取设备列表 +export function getDeviceList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/devices", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelection/index.module.scss b/nkebao/src/components/DeviceSelection/index.module.scss new file mode 100644 index 000000000..d457fa24a --- /dev/null +++ b/nkebao/src/components/DeviceSelection/index.module.scss @@ -0,0 +1,188 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + z-index: 10; + font-size: 18px; +} +.input { + padding-left: 38px !important; + height: 56px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100vh; + background: #fff; +} +.popupHeader { + padding: 16px; + border-bottom: 1px solid #f0f0f0; +} +.popupTitle { + font-size: 20px; + font-weight: 600; + text-align: center; +} +.popupSearchRow { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; +} +.popupSearchInputWrap { + position: relative; + flex: 1; +} +.popupSearchInput { + padding-left: 36px !important; + border-radius: 12px !important; + height: 44px; + font-size: 15px; + border: 1px solid #e5e6eb !important; + background: #f8f9fa; +} +.statusSelect { + width: 120px; + height: 40px; + border-radius: 8px; + border: 1px solid #e5e6eb; + font-size: 15px; + padding: 0 10px; + background: #fff; +} +.deviceList { + flex: 1; + overflow-y: auto; +} +.deviceListInner { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; +} +.deviceItem { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border-radius: 12px; + border: 1px solid #f0f0f0; + background: #fff; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.deviceCheckbox { + margin-top: 4px; +} +.deviceInfo { + flex: 1; +} +.deviceInfoRow { + display: flex; + align-items: center; + justify-content: space-between; +} +.deviceName { + font-weight: 500; + font-size: 16px; + color: #222; +} +.statusOnline { + width: 56px; + height: 24px; + border-radius: 12px; + background: #52c41a; + color: #fff; + font-size: 13px; + display: flex; + align-items: center; + justify-content: center; +} +.statusOffline { + width: 56px; + height: 24px; + border-radius: 12px; + background: #e5e6eb; + color: #888; + font-size: 13px; + display: flex; + align-items: center; + justify-content: center; +} +.deviceInfoDetail { + font-size: 13px; + color: #888; + margin-top: 4px; +} +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.popupFooter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-top: 1px solid #f0f0f0; + background: #fff; +} +.selectedCount { + font-size: 14px; + color: #888; +} +.footerBtnGroup { + display: flex; + gap: 12px; +} +.refreshBtn { + width: 36px; + height: 36px; +} +.paginationRow { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} +.totalCount { + font-size: 14px; + color: #888; +} +.paginationControls { + display: flex; + align-items: center; + gap: 8px; +} +.pageBtn { + padding: 0 8px; + height: 32px; + min-width: 32px; + border-radius: 16px; +} +.pageInfo { + font-size: 14px; + color: #222; + margin: 0 8px; +} diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx new file mode 100644 index 000000000..a62625d23 --- /dev/null +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -0,0 +1,380 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; +import { getDeviceList } from "./api"; +import style from "./index.module.scss"; +import { DeleteOutlined } from "@ant-design/icons"; + +// 设备选择项接口 +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +// 组件属性接口 +interface DeviceSelectionProps { + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + placeholder?: string; + className?: string; + mode?: "input" | "dialog"; // 新增,默认input + open?: boolean; // 仅mode=dialog时生效 + onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效 + selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 + showInput?: boolean; // 新增 + showSelectedList?: boolean; // 新增 + readonly?: boolean; // 新增 +} + +const PAGE_SIZE = 20; + +const DeviceSelection: React.FC = ({ + selectedDevices, + onSelect, + placeholder = "选择设备", + className = "", + mode = "input", + open, + onOpenChange, + selectedListMaxHeight = 300, // 默认300 + showInput = true, + showSelectedList = true, + readonly = false, +}) => { + // 弹窗控制 + const [popupVisible, setPopupVisible] = useState(false); + const isDialog = mode === "dialog"; + const realVisible = isDialog ? !!open : popupVisible; + const setRealVisible = (v: boolean) => { + if (isDialog && onOpenChange) onOpenChange(v); + if (!isDialog) setPopupVisible(v); + }; + + // 设备数据 + const [devices, setDevices] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + + // 获取设备列表,支持keyword和分页 + const fetchDevices = useCallback( + async (keyword: string = "", page: number = 1) => { + setLoading(true); + try { + const res = await getDeviceList({ + page, + limit: PAGE_SIZE, + keyword: keyword.trim() || undefined, + }); + if (res && Array.isArray(res.list)) { + setDevices( + res.list.map((d: any) => ({ + id: d.id?.toString() || "", + name: d.memo || d.imei || "", + imei: d.imei || "", + wechatId: d.wechatId || "", + status: d.alive === 1 ? "online" : "offline", + wxid: d.wechatId || "", + nickname: d.nickname || "", + usedInPlans: d.usedInPlans || 0, + })) + ); + setTotal(res.total || 0); + } + } catch (error) { + console.error("获取设备列表失败:", error); + } finally { + setLoading(false); + } + }, + [] + ); + + // 打开弹窗时获取第一页 + const openPopup = () => { + if (readonly) return; + setSearchQuery(""); + setCurrentPage(1); + setRealVisible(true); + fetchDevices("", 1); + }; + + // 搜索防抖 + useEffect(() => { + if (!realVisible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchDevices(searchQuery, 1); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, realVisible, fetchDevices]); + + // 翻页时重新请求 + useEffect(() => { + if (!realVisible) return; + fetchDevices(searchQuery, currentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]); + + // 过滤设备(只保留状态过滤) + const filteredDevices = devices.filter((device) => { + const matchesStatus = + statusFilter === "all" || + (statusFilter === "online" && device.status === "online") || + (statusFilter === "offline" && device.status === "offline"); + return matchesStatus; + }); + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedDevices.length === 0) return ""; + return `已选择 ${selectedDevices.length} 个设备`; + }; + + // 获取已选设备详细信息 + const selectedDeviceObjs = selectedDevices + .map((id) => devices.find((d) => d.id === id)) + .filter(Boolean) as DeviceSelectionItem[]; + + // 删除已选设备 + const handleRemoveDevice = (id: string) => { + if (readonly) return; + onSelect(selectedDevices.filter((d) => d !== id)); + }; + + // 弹窗内容 + const popupContent = ( +
+
+
选择设备
+
+
+
+ + setSearchQuery(val)} + className={style.popupSearchInput} + /> +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+ ); + + return ( + <> + {/* mode=input 显示输入框,mode=dialog不显示 */} + {mode === "input" && showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选设备列表窗口 */} + {mode === "input" && + showSelectedList && + selectedDeviceObjs.length > 0 && ( +
+ {selectedDeviceObjs.map((device) => ( +
+
+ {device.name} +
+ {!readonly && ( +
+ ))} +
+ )} + {/* 弹窗 */} + setRealVisible(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + {popupContent} + + + ); +}; + +export default DeviceSelection; diff --git a/nkebao/src/components/DeviceSelection/selectionPopup.tsx b/nkebao/src/components/DeviceSelection/selectionPopup.tsx new file mode 100644 index 000000000..8f77eabcf --- /dev/null +++ b/nkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -0,0 +1,197 @@ +import React from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input, Button, Checkbox, Popup } from "antd-mobile"; +import style from "./index.module.scss"; + +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +interface SelectionPopupProps { + visible: boolean; + onClose: () => void; + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + devices: DeviceSelectionItem[]; + loading: boolean; + searchQuery: string; + setSearchQuery: (v: string) => void; + statusFilter: string; + setStatusFilter: (v: string) => void; + onRefresh: () => void; + filteredDevices: DeviceSelectionItem[]; + total: number; + currentPage: number; + totalPages: number; + setCurrentPage: (v: number) => void; + onCancel: () => void; + onConfirm: () => void; +} + +const SelectionPopup: React.FC = ({ + visible, + onClose, + selectedDevices, + onSelect, + devices, + loading, + searchQuery, + setSearchQuery, + statusFilter, + setStatusFilter, + onRefresh, + filteredDevices, + total, + currentPage, + totalPages, + setCurrentPage, + onCancel, + onConfirm, +}) => { + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + return ( + {}} + position="bottom" + bodyStyle={{ height: "100vh" }} + closeOnMaskClick={false} + > +
+
+
选择设备
+
+
+
+ + +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ ); +}; + +export default SelectionPopup; diff --git a/nkebao/src/components/FriendSelection/api.ts b/nkebao/src/components/FriendSelection/api.ts new file mode 100644 index 000000000..56066f31a --- /dev/null +++ b/nkebao/src/components/FriendSelection/api.ts @@ -0,0 +1,11 @@ +import request from "@/api/request"; + +// 获取好友列表 +export function getFriendList(params: { + page: number; + limit: number; + deviceIds?: string; // 逗号分隔 + keyword?: string; +}) { + return request("/v1/friend", params, "GET"); +} diff --git a/nkebao/src/components/FriendSelection/index.module.scss b/nkebao/src/components/FriendSelection/index.module.scss new file mode 100644 index 000000000..663353e8d --- /dev/null +++ b/nkebao/src/components/FriendSelection/index.module.scss @@ -0,0 +1,231 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; +} +.input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100vh; + background: #fff; +} +.popupHeader { + padding: 24px; +} +.popupTitle { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; +} +.searchWrapper { + position: relative; + margin-bottom: 16px; +} +.searchInput { + padding-left: 40px !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + border-radius: 24px !important; + border: 1px solid #e5e6eb !important; + font-size: 15px; + background: #f8f9fa; +} +.searchIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 16px; +} +.clearBtn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + height: 24px; + width: 24px; + border-radius: 50%; + min-width: 24px; +} + +.friendList { + flex: 1; + overflow-y: auto; +} +.friendListInner { + border-top: 1px solid #f0f0f0; +} +.friendItem { + display: flex; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.radioWrapper { + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.radioSelected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #1890ff; + display: flex; + align-items: center; + justify-content: center; +} +.radioUnselected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #e5e6eb; + display: flex; + align-items: center; + justify-content: center; +} +.radioDot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #1890ff; +} +.friendInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} +.friendAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + font-weight: 500; + overflow: hidden; +} +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} +.friendDetail { + flex: 1; +} +.friendName { + font-weight: 500; + font-size: 16px; + color: #222; + margin-bottom: 2px; +} +.friendId { + font-size: 13px; + color: #888; + margin-bottom: 2px; +} +.friendCustomer { + font-size: 13px; + color: #bdbdbd; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.emptyText { + color: #888; + font-size: 15px; +} + +.paginationRow { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} +.totalCount { + font-size: 14px; + color: #888; +} +.paginationControls { + display: flex; + align-items: center; + gap: 8px; +} +.pageBtn { + padding: 0 8px; + height: 32px; + min-width: 32px; +} +.pageInfo { + font-size: 14px; + color: #222; +} + +.popupFooter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-top: 1px solid #f0f0f0; + background: #fff; +} +.selectedCount { + font-size: 14px; + color: #888; +} +.footerBtnGroup { + display: flex; + gap: 12px; +} +.cancelBtn { + padding: 0 24px; + border-radius: 24px; + border: 1px solid #e5e6eb; +} +.confirmBtn { + padding: 0 24px; + border-radius: 24px; +} diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx new file mode 100644 index 000000000..51598c531 --- /dev/null +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -0,0 +1,422 @@ +import React, { useState, useEffect } from "react"; +import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Popup, Toast } from "antd-mobile"; +import { Button, Input } from "antd"; +import { getFriendList } from "./api"; +import style from "./index.module.scss"; + +// 微信好友接口类型 +interface WechatFriend { + id: string; + nickname: string; + wechatId: string; + avatar: string; + customer: string; +} + +// 组件属性接口 +interface FriendSelectionProps { + selectedFriends: string[]; + onSelect: (friends: string[]) => void; + onSelectDetail?: (friends: WechatFriend[]) => void; + deviceIds?: string[]; + enableDeviceFilter?: boolean; + placeholder?: string; + className?: string; + visible?: boolean; // 新增 + onVisibleChange?: (visible: boolean) => void; // 新增 + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: (selectedIds: string[], selectedItems: WechatFriend[]) => void; // 新增 +} + +export default function FriendSelection({ + selectedFriends, + onSelect, + onSelectDetail, + deviceIds = [], + enableDeviceFilter = true, + placeholder = "选择微信好友", + className = "", + visible, + onVisibleChange, + selectedListMaxHeight = 300, + showInput = true, + showSelectedList = true, + readonly = false, + onConfirm, +}: FriendSelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + const [friends, setFriends] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalFriends, setTotalFriends] = useState(0); + const [loading, setLoading] = useState(false); + + // 受控弹窗逻辑 + const realVisible = visible !== undefined ? visible : popupVisible; + const setRealVisible = (v: boolean) => { + if (onVisibleChange) onVisibleChange(v); + if (visible === undefined) setPopupVisible(v); + }; + + // 打开弹窗并请求第一页好友 + const openPopup = () => { + if (readonly) return; + setCurrentPage(1); + setSearchQuery(""); + setRealVisible(true); + fetchFriends(1, ""); + }; + + // 当页码变化时,拉取对应页数据(弹窗已打开时) + useEffect(() => { + if (realVisible && currentPage !== 1) { + fetchFriends(currentPage, searchQuery); + } + }, [currentPage, realVisible, searchQuery]); + + // 搜索防抖 + useEffect(() => { + if (!realVisible) return; + + const timer = setTimeout(() => { + setCurrentPage(1); + fetchFriends(1, searchQuery); + }, 500); + + return () => clearTimeout(timer); + }, [searchQuery, realVisible]); + + // 获取好友列表API - 添加 keyword 参数 + const fetchFriends = async (page: number, keyword: string = "") => { + setLoading(true); + try { + let params: any = { + page, + limit: 20, + }; + + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + + if (enableDeviceFilter) { + if (deviceIds.length === 0) { + setFriends([]); + setTotalFriends(0); + setTotalPages(1); + setLoading(false); + return; + } + params.deviceIds = deviceIds.join(","); + } + + const res = await getFriendList(params); + + if (res && Array.isArray(res.list)) { + setFriends( + res.list.map((friend: any) => ({ + id: friend.id?.toString() || "", + nickname: friend.nickname || "", + wechatId: friend.wechatId || "", + avatar: friend.avatar || "", + customer: friend.customer || "", + })) + ); + setTotalFriends(res.total || 0); + setTotalPages(Math.ceil((res.total || 0) / 20)); + } + } catch (error) { + console.error("获取好友列表失败:", error); + Toast.show({ content: "获取好友列表失败", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 处理好友选择 + const handleFriendToggle = (friendId: string) => { + let newIds: string[]; + if (selectedFriends.includes(friendId)) { + newIds = selectedFriends.filter((id) => id !== friendId); + } else { + newIds = [...selectedFriends, friendId]; + } + onSelect(newIds); + if (onSelectDetail) { + const selectedObjs = friends.filter((f) => newIds.includes(f.id)); + onSelectDetail(selectedObjs); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedFriends.length === 0) return ""; + return `已选择 ${selectedFriends.length} 个好友`; + }; + + // 获取已选好友详细信息 + const selectedFriendObjs = selectedFriends + .map((id) => friends.find((f) => f.id === id)) + .filter(Boolean) as WechatFriend[]; + + // 删除已选好友 + const handleRemoveFriend = (id: string) => { + if (readonly) return; + onSelect(selectedFriends.filter((f) => f !== id)); + }; + + // 确认按钮逻辑 + const handleConfirm = () => { + setRealVisible(false); + if (onConfirm) { + onConfirm(selectedFriends, selectedFriendObjs); + } + }; + + // 清空搜索 + const handleClearSearch = () => { + setSearchQuery(""); + setCurrentPage(1); + fetchFriends(1, ""); + }; + + return ( + <> + {/* 输入框 */} + {showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选好友列表窗口 */} + {showSelectedList && selectedFriendObjs.length > 0 && ( +
+ {selectedFriendObjs.map((friend) => ( +
+
+ {friend.nickname || friend.wechatId || friend.id} +
+ {!readonly && ( +
+ ))} +
+ )} + {/* 弹窗 */} + setRealVisible(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > +
+
+
选择微信好友
+
+ setSearchQuery(e.target.value)} + disabled={readonly} + prefix={} + allowClear + size="large" + /> + {searchQuery && !readonly && ( +
+
+
+ {loading ? ( +
+
加载中...
+
+ ) : friends.length > 0 ? ( +
+ {friends.map((friend) => ( + + ))} +
+ ) : ( +
+
+ {deviceIds.length === 0 + ? "请先选择设备" + : searchQuery + ? `没有找到包含"${searchQuery}"的好友` + : "没有找到好友"} +
+
+ )} +
+ {/* 分页栏 */} +
+
总计 {totalFriends} 个好友
+
+ + + {currentPage} / {totalPages} + + +
+
+ {/* 底部按钮栏 */} +
+
+ 已选择 {selectedFriends.length} 个好友 +
+
+ + +
+
+
+
+ + ); +} diff --git a/nkebao/src/components/GroupSelection/api.ts b/nkebao/src/components/GroupSelection/api.ts new file mode 100644 index 000000000..af1ea70c1 --- /dev/null +++ b/nkebao/src/components/GroupSelection/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取群组列表 +export function getGroupList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/chatroom", params, "GET"); +} diff --git a/nkebao/src/components/GroupSelection/index.module.scss b/nkebao/src/components/GroupSelection/index.module.scss new file mode 100644 index 000000000..d959497ab --- /dev/null +++ b/nkebao/src/components/GroupSelection/index.module.scss @@ -0,0 +1,222 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; +} +.input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100vh; + background: #fff; +} +.popupHeader { + padding: 24px; +} +.popupTitle { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; +} +.searchWrapper { + position: relative; + margin-bottom: 16px; +} +.searchInput { + padding-left: 40px !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + border-radius: 24px !important; + border: 1px solid #e5e6eb !important; + font-size: 15px; + background: #f8f9fa; +} +.searchIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 16px; +} +.clearBtn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + height: 24px; + width: 24px; + border-radius: 50%; + min-width: 24px; +} + +.groupList { + flex: 1; + overflow-y: auto; +} +.groupListInner { + border-top: 1px solid #f0f0f0; +} +.groupItem { + display: flex; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.radioWrapper { + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.radioSelected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #1890ff; + display: flex; + align-items: center; + justify-content: center; +} +.radioUnselected { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #e5e6eb; + display: flex; + align-items: center; + justify-content: center; +} +.radioDot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #1890ff; +} +.groupInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} +.groupAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + font-weight: 500; + overflow: hidden; +} +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} +.groupDetail { + flex: 1; +} +.groupName { + font-weight: 500; + font-size: 16px; + color: #222; + margin-bottom: 2px; +} +.groupId { + font-size: 13px; + color: #888; + margin-bottom: 2px; +} +.groupOwner { + font-size: 13px; + color: #bdbdbd; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.emptyText { + color: #888; + font-size: 15px; +} + +.paginationRow { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} +.totalCount { + font-size: 14px; + color: #888; +} +.paginationControls { + display: flex; + align-items: center; + gap: 8px; +} +.pageBtn { + padding: 0 8px; + height: 32px; + min-width: 32px; +} +.pageInfo { + font-size: 14px; + color: #222; +} + +.popupFooter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-top: 1px solid #f0f0f0; + background: #fff; +} +.selectedCount { + font-size: 14px; + color: #888; +} +.footerBtnGroup { + display: flex; + gap: 12px; +} diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx new file mode 100644 index 000000000..15d2222a8 --- /dev/null +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -0,0 +1,413 @@ +import React, { useState, useEffect } from "react"; +import { + SearchOutlined, + CloseOutlined, + LeftOutlined, + RightOutlined, + DeleteOutlined, +} from "@ant-design/icons"; +import { Button as AntdButton, Input as AntdInput } from "antd"; +import { Popup, Toast } from "antd-mobile"; +import { getGroupList } from "./api"; +import style from "./index.module.scss"; + +// 群组接口类型 +interface WechatGroup { + id: string; + chatroomId: string; + name: string; + avatar: string; + ownerWechatId: string; + ownerNickname: string; + ownerAvatar: string; +} + +// 组件属性接口 +interface GroupSelectionProps { + selectedGroups: string[]; + onSelect: (groups: string[]) => void; + onSelectDetail?: (groups: WechatGroup[]) => void; + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: (selectedIds: string[], selectedItems: WechatGroup[]) => void; // 新增 +} + +export default function GroupSelection({ + selectedGroups, + onSelect, + onSelectDetail, + placeholder = "选择群聊", + className = "", + visible, + onVisibleChange, + selectedListMaxHeight = 300, + showInput = true, + showSelectedList = true, + readonly = false, + onConfirm, +}: GroupSelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + const [groups, setGroups] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalGroups, setTotalGroups] = useState(0); + const [loading, setLoading] = useState(false); + + // 获取已选群聊详细信息 + const selectedGroupObjs = selectedGroups + .map((id) => groups.find((g) => g.id === id)) + .filter(Boolean) as WechatGroup[]; + + // 删除已选群聊 + const handleRemoveGroup = (id: string) => { + if (readonly) return; + onSelect(selectedGroups.filter((g) => g !== id)); + }; + + // 受控弹窗逻辑 + const realVisible = visible !== undefined ? visible : popupVisible; + const setRealVisible = (v: boolean) => { + if (onVisibleChange) onVisibleChange(v); + if (visible === undefined) setPopupVisible(v); + }; + + // 打开弹窗 + const openPopup = () => { + if (readonly) return; + setCurrentPage(1); + setSearchQuery(""); + setRealVisible(true); + fetchGroups(1, ""); + }; + + // 当页码变化时,拉取对应页数据(弹窗已打开时) + useEffect(() => { + if (realVisible && currentPage !== 1) { + fetchGroups(currentPage, searchQuery); + } + }, [currentPage, realVisible, searchQuery]); + + // 搜索防抖 + useEffect(() => { + if (!realVisible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchGroups(1, searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, realVisible]); + + // 获取群组列表API - 支持keyword + const fetchGroups = async (page: number, keyword: string = "") => { + setLoading(true); + try { + const params: any = { + page, + limit: 20, + }; + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + + const res = await getGroupList(params); + + if (res && Array.isArray(res.list)) { + setGroups( + res.list.map((group: any) => ({ + id: group.id?.toString() || "", + chatroomId: group.chatroomId || "", + name: group.name || "", + avatar: group.avatar || "", + ownerWechatId: group.ownerWechatId || "", + ownerNickname: group.ownerNickname || "", + ownerAvatar: group.ownerAvatar || "", + })) + ); + setTotalGroups(res.total || 0); + setTotalPages(Math.ceil((res.total || 0) / 20)); + } + } catch (error) { + console.error("获取群组列表失败:", error); + Toast.show({ content: "获取群组列表失败", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 处理群组选择 + const handleGroupToggle = (groupId: string) => { + let newIds: string[]; + if (selectedGroups.includes(groupId)) { + newIds = selectedGroups.filter((id) => id !== groupId); + } else { + newIds = [...selectedGroups, groupId]; + } + onSelect(newIds); + if (onSelectDetail) { + const selectedObjs = groups.filter((g) => newIds.includes(g.id)); + onSelectDetail(selectedObjs); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedGroups.length === 0) return ""; + return `已选择 ${selectedGroups.length} 个群聊`; + }; + + // 确认按钮逻辑 + const handleConfirm = () => { + setRealVisible(false); + if (onConfirm) { + onConfirm(selectedGroups, selectedGroupObjs); + } + }; + + // 清空搜索 + const handleClearSearch = () => { + setSearchQuery(""); + setCurrentPage(1); + fetchGroups(1, ""); + }; + + return ( + <> + {/* 输入框 */} + {showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选群聊列表窗口 */} + {showSelectedList && selectedGroupObjs.length > 0 && ( +
+ {selectedGroupObjs.map((group) => ( +
+
+ {group.name || group.chatroomId || group.id} +
+ {!readonly && ( + } + size="small" + style={{ + marginLeft: 4, + color: "#ff4d4f", + border: "none", + background: "none", + minWidth: 24, + height: 24, + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + onClick={() => handleRemoveGroup(group.id)} + /> + )} +
+ ))} +
+ )} + {/* 弹窗 */} + setRealVisible(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > +
+
+
选择群聊
+
+ setSearchQuery(e.target.value)} + disabled={readonly} + prefix={} + allowClear + size="large" + /> + + {searchQuery && !readonly && ( + } + size="small" + className={style.clearBtn} + onClick={handleClearSearch} + style={{ + color: "#ff4d4f", + border: "none", + background: "none", + minWidth: 24, + height: 24, + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + /> + )} +
+
+
+ {loading ? ( +
+
加载中...
+
+ ) : groups.length > 0 ? ( +
+ {groups.map((group) => ( + + ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的群聊` + : "没有找到群聊"} +
+
+ )} +
+ {/* 分页栏 */} +
+
总计 {totalGroups} 个群聊
+
+ setCurrentPage(Math.max(1, currentPage - 1))} + disabled={currentPage === 1 || loading} + className={style.pageBtn} + style={{ borderRadius: 16 }} + > + + + + {currentPage} / {totalPages} + + + setCurrentPage(Math.min(totalPages, currentPage + 1)) + } + disabled={currentPage === totalPages || loading} + className={style.pageBtn} + style={{ borderRadius: 16 }} + > + + +
+
+ {/* 底部按钮栏 */} +
+
+ 已选择 {selectedGroups.length} 个群聊 +
+
+ setRealVisible(false)}> + 取消 + + + 确定 + +
+
+
+
+ + ); +} diff --git a/nkebao/src/components/MeauMobile/MeauMoible.tsx b/nkebao/src/components/MeauMobile/MeauMoible.tsx index 2a84a9dc7..36b758c6a 100644 --- a/nkebao/src/components/MeauMobile/MeauMoible.tsx +++ b/nkebao/src/components/MeauMobile/MeauMoible.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import { TabBar } from "antd-mobile"; import { PieOutline, UserOutline } from "antd-mobile-icons"; import { HomeOutlined, TeamOutlined } from "@ant-design/icons"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; const tabs = [ { @@ -12,13 +12,13 @@ const tabs = [ path: "/", }, { - key: "scene", + key: "scenarios", title: "场景获客", icon: , path: "/scenarios", }, { - key: "work", + key: "workspace", title: "工作台", icon: , path: "/workspace", @@ -31,38 +31,18 @@ const tabs = [ }, ]; -// 需要展示菜单的路由白名单(可根据实际业务调整) -const menuPaths = ["/", "/scenarios", "/workspace", "/mine"]; +interface MeauMobileProps { + activeKey: string; +} -const MeauMobile: React.FC = () => { - const location = useLocation(); +const MeauMobile: React.FC = ({ activeKey }) => { const navigate = useNavigate(); - const [activeKey, setActiveKey] = useState("home"); - - // 根据当前路由自动设置 activeKey,支持嵌套路由 - useEffect(() => { - const found = tabs.find((tab) => - tab.path === "/" - ? location.pathname === "/" - : location.pathname.startsWith(tab.path) - ); - if (found) setActiveKey(found.key); - }, [location.pathname]); - - // 判断当前路由是否需要展示菜单 - const showMenu = menuPaths.some((path) => - path === "/" - ? location.pathname === "/" - : location.pathname.startsWith(path) - ); - if (!showMenu) return null; return ( { - setActiveKey(key); const tab = tabs.find((t) => t.key === key); if (tab && tab.path) navigate(tab.path); }} diff --git a/nkebao/src/components/SelectionTest.tsx b/nkebao/src/components/SelectionTest.tsx new file mode 100644 index 000000000..425f9ae21 --- /dev/null +++ b/nkebao/src/components/SelectionTest.tsx @@ -0,0 +1,68 @@ +import React, { useState } from "react"; +import DeviceSelection from "./DeviceSelection"; +import FriendSelection from "./FriendSelection"; +import GroupSelection from "./GroupSelection"; +import { Button, Space } from "antd-mobile"; + +export default function SelectionTest() { + // 设备选择 + const [selectedDevices, setSelectedDevices] = useState([]); + const [deviceDialogOpen, setDeviceDialogOpen] = useState(false); + + // 好友选择 + const [selectedFriends, setSelectedFriends] = useState([]); + const [friendDialogOpen, setFriendDialogOpen] = useState(false); + + // 群组选择 + const [selectedGroups, setSelectedGroups] = useState([]); + const [groupDialogOpen, setGroupDialogOpen] = useState(false); + + return ( +
+

选择弹窗测试

+ +
+ DeviceSelection(内嵌输入框+弹窗) + +
+ +
+ FriendSelection + + +
+
+ GroupSelection + + +
+
+
+
已选设备ID: {selectedDevices.join(", ")}
+
已选好友ID: {selectedFriends.join(", ")}
+
已选群组ID: {selectedGroups.join(", ")}
+
+
+ ); +} diff --git a/nkebao/src/pages/devices/DeviceDetail.tsx b/nkebao/src/pages/devices/DeviceDetail.tsx index 69876d6e0..a67f8883e 100644 --- a/nkebao/src/pages/devices/DeviceDetail.tsx +++ b/nkebao/src/pages/devices/DeviceDetail.tsx @@ -1,28 +1,369 @@ -import React from "react"; -import { NavBar } from "antd-mobile"; +import React, { useEffect, useState, useCallback, useRef } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile"; +import { SettingOutlined, RedoOutlined } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchDeviceDetail, + fetchDeviceRelatedAccounts, + fetchDeviceHandleLogs, + updateDeviceTaskConfig, +} from "@/api/devices"; +import type { Device, WechatAccount, HandleLog } from "@/types/device"; const DeviceDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [device, setDevice] = useState(null); + const [tab, setTab] = useState("info"); + const [accounts, setAccounts] = useState([]); + const [accountsLoading, setAccountsLoading] = useState(false); + const [logs, setLogs] = useState([]); + const [logsLoading, setLogsLoading] = useState(false); + const [featureSaving, setFeatureSaving] = useState<{ [k: string]: boolean }>( + {} + ); + + // 获取设备详情 + const loadDetail = useCallback(async () => { + if (!id) return; + setLoading(true); + try { + const res = await fetchDeviceDetail(id); + setDevice(res); + } catch (e: any) { + Toast.show({ content: e.message || "获取设备详情失败", position: "top" }); + } finally { + setLoading(false); + } + }, [id]); + + // 获取关联账号 + const loadAccounts = useCallback(async () => { + if (!id) return; + setAccountsLoading(true); + try { + const res = await fetchDeviceRelatedAccounts(id); + setAccounts(Array.isArray(res.accounts) ? res.accounts : []); + } catch (e: any) { + Toast.show({ content: e.message || "获取关联账号失败", position: "top" }); + } finally { + setAccountsLoading(false); + } + }, [id]); + + // 获取操作日志 + const loadLogs = useCallback(async () => { + if (!id) return; + setLogsLoading(true); + try { + const res = await fetchDeviceHandleLogs(id, 1, 20); + setLogs(Array.isArray(res.list) ? res.list : []); + } catch (e: any) { + Toast.show({ content: e.message || "获取操作日志失败", position: "top" }); + } finally { + setLogsLoading(false); + } + }, [id]); + + useEffect(() => { + loadDetail(); + // eslint-disable-next-line + }, [id]); + + useEffect(() => { + if (tab === "accounts") loadAccounts(); + if (tab === "logs") loadLogs(); + // eslint-disable-next-line + }, [tab]); + + // 功能开关 + const handleFeatureChange = async ( + feature: keyof Device["features"], + checked: boolean + ) => { + if (!id) return; + setFeatureSaving((prev) => ({ ...prev, [feature]: true })); + try { + await updateDeviceTaskConfig({ deviceId: id, [feature]: checked }); + setDevice((prev) => + prev + ? { + ...prev, + features: { ...prev.features, [feature]: checked }, + } + : prev + ); + Toast.show({ + content: `${getFeatureName(feature)}已${checked ? "开启" : "关闭"}`, + }); + } catch (e: any) { + Toast.show({ content: e.message || "设置失败", position: "top" }); + } finally { + setFeatureSaving((prev) => ({ ...prev, [feature]: false })); + } + }; + + const getFeatureName = (feature: string) => { + const map: Record = { + autoAddFriend: "自动加好友", + autoReply: "自动回复", + momentsSync: "朋友圈同步", + aiChat: "AI会话", + }; + return map[feature] || feature; + }; + return ( navigate(-1)} style={{ background: "#fff" }} - onBack={() => window.history.back()} + right={ + + } > -
+ 设备详情 -
+ } - footer={} + loading={loading} > -
-

设备详情页面

-

此页面正在开发中...

-
+ {!device ? ( +
+ +
正在加载设备信息...
+
+ ) : ( +
+ {/* 基本信息卡片 */} +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} +
+
+ {/* 标签页 */} + + + + + + {/* 功能开关 */} + {tab === "info" && ( +
+ {["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map( + (f) => ( +
+
+
{getFeatureName(f)}
+
+ + handleFeatureChange( + f as keyof Device["features"], + checked + ) + } + /> +
+ ) + )} +
+ )} + {/* 关联账号 */} + {tab === "accounts" && ( +
+ {accountsLoading ? ( +
+ +
+ ) : accounts.length === 0 ? ( +
+ 暂无关联微信账号 +
+ ) : ( +
+ {accounts.map((acc) => ( +
+ {acc.nickname} +
+
{acc.nickname}
+
+ 微信号: {acc.wechatId} +
+
+ 好友数: {acc.totalFriend} +
+
+ 最后活跃: {acc.lastActive} +
+
+ + {acc.wechatAliveText} + +
+ ))} +
+ )} +
+ +
+
+ )} + {/* 操作日志 */} + {tab === "logs" && ( +
+ {logsLoading ? ( +
+ +
+ ) : logs.length === 0 ? ( +
+ 暂无操作日志 +
+ ) : ( +
+ {logs.map((log) => ( +
+
{log.content}
+
+ 操作人: {log.username} · {log.createTime} +
+
+ ))} +
+ )} +
+ +
+
+ )} +
+ )}
); }; diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 66895d54e..950c02cef 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,37 +1,431 @@ -import React from "react"; -import { NavBar, Button } from "antd-mobile"; -import { AddOutline } from "antd-mobile-icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; - -const Devices: React.FC = () => { - return ( - - 设备管理 - - } - right={ - - } - /> - } - footer={} - > -
-

设备管理页面

-

此页面正在开发中...

-
-
- ); -}; - -export default Devices; +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; +import { Button, Input, Pagination, Checkbox } from "antd"; +import { useNavigate } from "react-router-dom"; +import { AddOutline, DeleteOutline } from "antd-mobile-icons"; +import { + ReloadOutlined, + SearchOutlined, + QrcodeOutlined, + ArrowLeftOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchDeviceList, + fetchDeviceQRCode, + addDeviceByImei, + deleteDevice, +} from "@/api/devices"; +import type { Device } from "@/types/device"; +import { comfirm } from "@/utils/common"; + +const Devices: React.FC = () => { + // 设备列表相关 + const [devices, setDevices] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState<"all" | "online" | "offline">("all"); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [total, setTotal] = useState(0); + const [selected, setSelected] = useState<(string | number)[]>([]); + const observerRef = useRef(null); + const [usePagination, setUsePagination] = useState(true); // 新增:是否使用分页 + + // 添加设备弹窗 + const [addVisible, setAddVisible] = useState(false); + const [addTab, setAddTab] = useState("scan"); + const [qrLoading, setQrLoading] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [imei, setImei] = useState(""); + const [name, setName] = useState(""); + const [addLoading, setAddLoading] = useState(false); + + // 删除弹窗 + const [delVisible, setDelVisible] = useState(false); + const [delLoading, setDelLoading] = useState(false); + + const navigate = useNavigate(); + // 加载设备列表 + const loadDevices = useCallback( + async (reset = false) => { + if (loading) return; + setLoading(true); + try { + const params: any = { page: reset ? 1 : page, limit: 20 }; + if (search) params.keyword = search; + const res = await fetchDeviceList(params); + const list = Array.isArray(res.list) ? res.list : []; + setDevices((prev) => (reset ? list : [...prev, ...list])); + setTotal(res.total || 0); + setHasMore(list.length === 20); + if (reset) setPage(1); + } catch (e) { + Toast.show({ content: "获取设备列表失败", position: "top" }); + setHasMore(false); // 请求失败后不再继续请求 + } finally { + setLoading(false); + } + }, + [loading, search, page] + ); + + // 首次加载和搜索 + useEffect(() => { + loadDevices(true); + // eslint-disable-next-line + }, [search]); + + // 无限滚动 + useEffect(() => { + if (!hasMore || loading) return; + const observer = new window.IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading) { + setPage((p) => p + 1); + } + }, + { threshold: 0.5 } + ); + if (observerRef.current) observer.observe(observerRef.current); + return () => observer.disconnect(); + }, [hasMore, loading]); + + // 分页加载 + useEffect(() => { + if (page === 1) return; + loadDevices(); + // eslint-disable-next-line + }, [page]); + + // 状态筛选 + const filtered = devices.filter((d) => { + if (status === "all") return true; + if (status === "online") return d.status === "online" || d.alive === 1; + if (status === "offline") return d.status === "offline" || d.alive === 0; + return true; + }); + + // 获取二维码 + const handleGetQr = async () => { + setQrLoading(true); + setQrCode(null); + try { + const accountId = localStorage.getItem("s2_accountId") || ""; + if (!accountId) throw new Error("未获取到用户信息"); + const res = await fetchDeviceQRCode(accountId); + setQrCode(res.qrCode); + } catch (e: any) { + Toast.show({ content: e.message || "获取二维码失败", position: "top" }); + } finally { + setQrLoading(false); + } + }; + + // 手动添加设备 + const handleAddDevice = async () => { + if (!imei.trim() || !name.trim()) { + Toast.show({ content: "请填写完整信息", position: "top" }); + return; + } + setAddLoading(true); + try { + await addDeviceByImei(imei, name); + Toast.show({ content: "添加成功", position: "top" }); + setAddVisible(false); + setImei(""); + setName(""); + loadDevices(true); + } catch (e: any) { + Toast.show({ content: e.message || "添加失败", position: "top" }); + } finally { + setAddLoading(false); + } + }; + + // 删除设备 + const handleDelete = async () => { + setDelLoading(true); + try { + for (const id of selected) { + await deleteDevice(Number(id)); + } + Toast.show({ content: `删除成功`, position: "top" }); + setSelected([]); + loadDevices(true); + } catch (e: any) { + if (e) Toast.show({ content: e.message || "删除失败", position: "top" }); + } finally { + setDelLoading(false); + } + }; + + // 删除按钮点击 + const handleDeleteClick = async () => { + try { + await comfirm( + `将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`, + { title: "确认删除", confirmText: "确认删除", cancelText: "取消" } + ); + handleDelete(); + } catch { + // 用户取消,无需处理 + } + }; + + // 跳转详情 + const goDetail = (id: string | number) => { + window.location.href = `/devices/${id}`; + }; + + // 分页切换 + const handlePageChange = (p: number) => { + setPage(p); + loadDevices(true); + }; + + return ( + + + navigate(-1)} + /> + + } + style={{ background: "#fff" }} + right={ + + } + > + + 设备管理 + + + +
+ {/* 搜索栏 */} +
+ setSearch(e.target.value)} + prefix={} + allowClear + style={{ flex: 1 }} + /> + +
+ {/* 筛选和删除 */} +
+ setStatus(k as any)} + style={{ flex: 1 }} + > + + + + +
+ +
+
+
+ + } + footer={ +
+ +
+ } + loading={loading && devices.length === 0} + > +
+ {/* 设备列表 */} +
+ {filtered.map((device) => ( +
goDetail(device.id!)} + > + { + e.stopPropagation(); + setSelected((prev) => + e.target.checked + ? [...prev, device.id!] + : prev.filter((id) => id !== device.id) + ); + }} + onClick={(e) => e.stopPropagation()} + style={{ marginRight: 12 }} + /> +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ + {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} + +
+ ))} + + {/* 无限滚动提示(仅在不分页时显示) */} + {!usePagination && ( +
+ {loading && } + {!hasMore && devices.length > 0 && "没有更多设备了"} + {!hasMore && devices.length === 0 && "暂无设备"} +
+ )} +
+
+ {/* 添加设备弹窗 */} + setAddVisible(false)} + bodyStyle={{ + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + minHeight: 320, + }} + > +
+ + + + + {addTab === "scan" && ( +
+ + {qrCode && ( +
+ 二维码 +
+ 请用手机扫码添加设备 +
+
+ )} +
+ )} + {addTab === "manual" && ( +
+ setName(e.target.value)} + allowClear + /> + setImei(e.target.value)} + allowClear + /> + +
+ )} +
+
+
+ ); +}; + +export default Devices; diff --git a/nkebao/src/pages/home/index.module.scss b/nkebao/src/pages/home/index.module.scss index bd72ec6a0..ac10533ae 100644 --- a/nkebao/src/pages/home/index.module.scss +++ b/nkebao/src/pages/home/index.module.scss @@ -1,11 +1,8 @@ .home-page { padding: 12px; - background: #f8f6f3; - min-height: 100vh; } .content-wrapper { - padding: 12px; display: flex; flex-direction: column; gap: 12px; @@ -58,7 +55,6 @@ display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; - margin-bottom: 16px; } .stat-card { @@ -79,8 +75,8 @@ } .stat-label { - font-size: 16px; - color: #666; + font-size: 14px; + color: #333; line-height: 1.2; font-weight: bold; } @@ -159,7 +155,6 @@ background: white; border-radius: 12px; padding: 16px; - margin-bottom: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); transition: all 0.3s ease; diff --git a/nkebao/src/pages/mine/Profile.tsx b/nkebao/src/pages/mine/Profile.tsx new file mode 100644 index 000000000..bb302b3b9 --- /dev/null +++ b/nkebao/src/pages/mine/Profile.tsx @@ -0,0 +1,258 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ChevronRight, Settings, Bell, LogOut, Smartphone, MessageCircle, Database, FolderOpen } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { useAuth } from '@/contexts/AuthContext'; +import { useToast } from '@/components/ui/toast'; +import Layout from '@/components/Layout'; +import BottomNav from '@/components/BottomNav'; +import UnifiedHeader from '@/components/UnifiedHeader'; +import '@/components/Layout.css'; + +export default function Profile() { + const navigate = useNavigate(); + const { user, logout, isAuthenticated } = useAuth(); + const { toast } = useToast(); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const [userInfo, setUserInfo] = useState(null); + const [stats, setStats] = useState({ + devices: 12, + wechat: 25, + traffic: 8, + content: 156, + }); + + // 从localStorage获取用户信息 + useEffect(() => { + const userInfoStr = localStorage.getItem('userInfo'); + if (userInfoStr) { + setUserInfo(JSON.parse(userInfoStr)); + } + }, []); + + // 用户信息 + const currentUserInfo = { + name: userInfo?.username || user?.username || "卡若", + email: userInfo?.email || "zhangsan@example.com", + role: "管理员", + joinDate: "2023-01-15", + lastLogin: "2024-01-20 14:30", + }; + + // 功能模块数据 + const functionModules = [ + { + id: "devices", + title: "设备管理", + description: "管理您的设备和微信账号", + icon: , + count: stats.devices, + path: "/devices", + bgColor: "bg-blue-50", + }, + { + id: "wechat", + title: "微信号管理", + description: "管理微信账号和好友", + icon: , + count: stats.wechat, + path: "/wechat-accounts", + bgColor: "bg-green-50", + }, + { + id: "traffic", + title: "流量池", + description: "管理用户流量池和分组", + icon: , + count: stats.traffic, + path: "/traffic-pool", + bgColor: "bg-purple-50", + }, + { + id: "content", + title: "内容库", + description: "管理营销内容和素材", + icon: , + count: stats.content, + path: "/content", + bgColor: "bg-orange-50", + }, + ]; + + // 加载统计数据 + const loadStats = async () => { + try { + // 这里可以调用实际的API + // const [deviceStats, wechatStats, trafficStats, contentStats] = await Promise.allSettled([ + // getDeviceStats(), + // getWechatStats(), + // getTrafficStats(), + // getContentStats(), + // ]); + + // 暂时使用模拟数据 + setStats({ + devices: 12, + wechat: 25, + traffic: 8, + content: 156, + }); + } catch (error) { + console.error("加载统计数据失败:", error); + } + }; + + useEffect(() => { + loadStats(); + }, []); + + const handleLogout = () => { + // 清除本地存储的用户信息 + localStorage.removeItem('token'); + localStorage.removeItem('token_expired'); + localStorage.removeItem('s2_accountId'); + localStorage.removeItem('userInfo'); + setShowLogoutDialog(false); + logout(); + navigate('/login'); + toast({ + title: '退出成功', + description: '您已安全退出系统', + }); + }; + + const handleFunctionClick = (path: string) => { + navigate(path); + }; + + if (!isAuthenticated) { + return ( +
+
请先登录
+
+ ); + } + + return ( + console.log('Notifications'), + }, + { + type: 'icon', + icon: Settings, + onClick: () => console.log('Settings'), + }, + ]} + /> + } + footer={} + > +
+
+ {/* 用户信息卡片 */} + + +
+ + + + {currentUserInfo.name.charAt(0)} + + +
+
+

{currentUserInfo.name}

+ + {currentUserInfo.role} + +
+

{currentUserInfo.email}

+
+
最近登录: {currentUserInfo.lastLogin}
+
+
+
+ + +
+
+
+
+ + {/* 我的功能 */} + + +
+ {functionModules.map((module) => ( +
handleFunctionClick(module.path)} + > +
{module.icon}
+
+
{module.title}
+
{module.description}
+
+
+ + {module.count} + + +
+
+ ))} +
+
+
+ + {/* 退出登录 */} + +
+
+ + {/* 退出登录确认对话框 */} + + + + 确认退出登录 + + 您确定要退出登录吗?退出后需要重新登录才能使用完整功能。 + + +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/mine/index.module.scss b/nkebao/src/pages/mine/index.module.scss index 98aa403b9..fc1d92c7f 100644 --- a/nkebao/src/pages/mine/index.module.scss +++ b/nkebao/src/pages/mine/index.module.scss @@ -1,7 +1,5 @@ .mine-page { - padding: 16px; - background-color: #f5f5f5; - min-height: 100vh; + padding: 12px; } .user-card { @@ -59,22 +57,24 @@ .menu-card { margin-bottom: 16px; border-radius: 12px; - overflow: hidden; - - :global(.adm-card-body) { - padding: 0; + + :global(.adm-list-body) { + border: none; + } + + :global(.adm-card-body) { + padding: 14px 0 0 0; + } + :global(.adm-list-body-inner) { + margin-top: 0px; } - :global(.adm-list-item) { - padding: 16px; - border-bottom: 1px solid #f0f0f0; - - &:last-child { - border-bottom: none; - } - + padding: 0px; :global(.adm-list-item-content) { - padding: 0; + border: 1px solid #f0f0f0; + margin-bottom: 12px; + padding: 0 12px; + border-radius: 12px; } :global(.adm-list-item-content-prefix) { @@ -104,9 +104,6 @@ } } -.logout-section { - padding: 0 16px; -} .logout-btn { border-radius: 8px; diff --git a/nkebao/src/pages/mine/index.tsx b/nkebao/src/pages/mine/index.tsx index 9cdc3a288..b6298f529 100644 --- a/nkebao/src/pages/mine/index.tsx +++ b/nkebao/src/pages/mine/index.tsx @@ -1,78 +1,179 @@ -import React from "react"; -import { Card, NavBar, List, Button } from "antd-mobile"; +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Card, NavBar, List, Button, Dialog, Toast } from "antd-mobile"; import { - UserOutline, - AppOutline, - BellOutline, - HeartOutline, - StarOutline, - MessageOutline, - SendOutline, - MailOutline, -} from "antd-mobile-icons"; + LogoutOutlined, + PhoneOutlined, + MessageOutlined, + DatabaseOutlined, + FolderOpenOutlined, + BellOutlined, + SettingOutlined, +} from "@ant-design/icons"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; const Mine: React.FC = () => { - const userInfo = { - name: "张三", - avatar: "https://via.placeholder.com/60", - level: "VIP会员", - points: 1280, + const navigate = useNavigate(); + const [userInfo, setUserInfo] = useState(null); + const [stats, setStats] = useState({ + devices: 12, + wechat: 25, + traffic: 8, + content: 156, + }); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + + // 从localStorage获取用户信息 + useEffect(() => { + const userInfoStr = localStorage.getItem("userInfo"); + if (userInfoStr) { + setUserInfo(JSON.parse(userInfoStr)); + } + }, []); + + // 用户信息 + const currentUserInfo = { + name: userInfo?.username || "售前", + email: userInfo?.email || "zhangsan@example.com", + role: "管理员", + lastLogin: "2024-01-20 14:30", + avatar: userInfo?.avatar || "", }; - const menuItems = [ + // 功能模块数据 + const functionModules = [ { - icon: , - title: "个人资料", - subtitle: "修改个人信息", - path: "/profile", + id: "devices", + title: "设备管理", + description: "管理您的设备和微信账号", + icon: , + count: stats.devices, + path: "/devices", + bgColor: "#e6f7ff", + iconColor: "#1890ff", }, { - icon: , - title: "系统设置", - subtitle: "应用设置与偏好", - path: "/settings", + id: "wechat", + title: "微信号管理", + description: "管理微信账号和好友", + icon: , + count: stats.wechat, + path: "/wechat-accounts", + bgColor: "#f6ffed", + iconColor: "#52c41a", }, { - icon: , - title: "消息通知", - subtitle: "通知设置", - path: "/notifications", + id: "traffic", + title: "流量池", + description: "管理用户流量池和分组", + icon: , + count: stats.traffic, + path: "/traffic-pool", + bgColor: "#f9f0ff", + iconColor: "#722ed1", }, { - icon: , - title: "我的收藏", - subtitle: "收藏的内容", - path: "/favorites", - }, - { - icon: , - title: "我的评价", - subtitle: "查看评价记录", - path: "/reviews", - }, - { - icon: , - title: "意见反馈", - subtitle: "问题反馈与建议", - path: "/feedback", - }, - { - icon: , - title: "联系客服", - subtitle: "在线客服", - path: "/customer-service", - }, - { - icon: , - title: "关于我们", - subtitle: "版本信息", - path: "/about", + id: "content", + title: "内容库", + description: "管理营销内容和素材", + icon: , + count: stats.content, + path: "/content", + bgColor: "#fff7e6", + iconColor: "#fa8c16", }, ]; + // 加载统计数据 + const loadStats = async () => { + try { + // 这里可以调用实际的API + // const [deviceStats, wechatStats, trafficStats, contentStats] = await Promise.allSettled([ + // getDeviceStats(), + // getWechatStats(), + // getTrafficStats(), + // getContentStats(), + // ]); + + // 暂时使用模拟数据 + setStats({ + devices: 12, + wechat: 25, + traffic: 8, + content: 156, + }); + } catch (error) { + console.error("加载统计数据失败:", error); + } + }; + + useEffect(() => { + loadStats(); + }, []); + + const handleLogout = () => { + // 清除本地存储的用户信息 + localStorage.removeItem("token"); + localStorage.removeItem("token_expired"); + localStorage.removeItem("s2_accountId"); + localStorage.removeItem("userInfo"); + setShowLogoutDialog(false); + navigate("/login"); + Toast.show({ + content: "退出成功", + position: "top", + }); + }; + + const handleFunctionClick = (path: string) => { + navigate(path); + }; + + // 渲染用户头像 + const renderUserAvatar = () => { + if (currentUserInfo.avatar) { + return 头像; + } + return ( +
+ 售 +
+ ); + }; + + // 渲染功能模块图标 + const renderModuleIcon = (module: any) => ( +
+ {module.icon} +
+ ); + return ( { } - footer={} + footer={} >
{/* 用户信息卡片 */}
-
- 头像 -
+
{renderUserAvatar()}
-
{userInfo.name}
-
{userInfo.level}
-
- 积分: {userInfo.points} +
+
{currentUserInfo.name}
+ + {currentUserInfo.role} +
+
+ {currentUserInfo.email} +
+
+ 最近登录: {currentUserInfo.lastLogin} +
+
+
+ +
- {/* 菜单列表 */} + {/* 我的功能 */} - {menuItems.map((item, index) => ( + {functionModules.map((module) => ( + {module.count} + + } arrow - onClick={() => { - // 这里可以添加导航逻辑 - console.log(`点击了: ${item.title}`); - }} + onClick={() => handleFunctionClick(module.path)} /> ))} {/* 退出登录按钮 */} -
- -
+
+ + {/* 退出登录确认对话框 */} + setShowLogoutDialog(false)} + /> ); }; diff --git a/nkebao/src/pages/scenarios/Scenarios.tsx b/nkebao/src/pages/scenarios/Scenarios.tsx deleted file mode 100644 index 48d337601..000000000 --- a/nkebao/src/pages/scenarios/Scenarios.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const Scenarios: React.FC = () => { - return ( - - ); -}; - -export default Scenarios; diff --git a/nkebao/src/pages/scenarios/list/index.tsx b/nkebao/src/pages/scenarios/list/index.tsx index 97be9b464..24ddffe85 100644 --- a/nkebao/src/pages/scenarios/list/index.tsx +++ b/nkebao/src/pages/scenarios/list/index.tsx @@ -82,19 +82,12 @@ const Scene: React.FC = () => { -
场景获客
- } - footer={} >
{error}
@@ -113,21 +106,20 @@ const Scene: React.FC = () => { 场景获客
} + left={
场景获客
} right={ } > } - footer={} + footer={} >
diff --git a/nkebao/src/pages/scenarios/plan/list/index.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss index f8b8ddb01..4df03dc4b 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -2,18 +2,6 @@ padding:0 16px; } -.nav-title { - font-size: 18px; - font-weight: 600; - color: #333; -} - -.new-plan-btn { - font-size: 14px; - height: 32px; - padding: 0 12px; -} - .loading { display: flex; flex-direction: column; diff --git a/nkebao/src/pages/scenarios/plan/list/index.tsx b/nkebao/src/pages/scenarios/plan/list/index.tsx index ebeb8f468..7a62a842f 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -24,7 +24,7 @@ import { ClockCircleOutlined, DownOutlined, } from "@ant-design/icons"; -import { LeftOutline } from "antd-mobile-icons"; +import { ArrowLeftOutlined } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import { @@ -356,11 +356,11 @@ const ScenarioList: React.FC = () => { back={null} style={{ background: "#fff" }} left={ -
- - navigate(-1)} fontSize={24} /> - - {scenarioName} +
+ navigate(-1)} + />
} right={ @@ -368,12 +368,13 @@ const ScenarioList: React.FC = () => { size="small" color="primary" onClick={handleCreateNewPlan} - className={style["new-plan-btn"]} > 新建计划 } - /> + > + {scenarioName} + {/* 搜索栏 */}
diff --git a/nkebao/src/pages/scenarios/plan/new/index.api.ts b/nkebao/src/pages/scenarios/plan/new/index.api.ts new file mode 100644 index 000000000..a4e06ebca --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/new/index.api.ts @@ -0,0 +1,53 @@ +import request from "@/api/request"; +// 获取场景类型列表 +export function getScenarioTypes() { + return request("/v1/scenarios/types", undefined, "GET"); +} + +// 创建计划 +export function createPlan(data: any) { + return request("/v1/scenarios/plans", data, "POST"); +} + +// 更新计划 +export function updatePlan(planId: string, data: any) { + return request(`/v1/scenarios/plans/${planId}`, data, "PUT"); +} + +// 获取计划详情 +export function getPlanDetail(planId: string) { + return request(`/v1/scenarios/plans/${planId}`, undefined, "GET"); +} + +// PlanDetail 类型定义(可根据实际接口返回结构补充字段) +export interface PlanDetail { + name: string; + scenario: number; + posters: any[]; + device: string[]; + remarkType: string; + greeting: string; + addInterval: number; + startTime: string; + endTime: string; + enabled: boolean; + sceneId: string | number; + remarkFormat: string; + addFriendInterval: number; + // 其它字段可扩展 + [key: string]: any; +} + +// 兼容旧代码的接口命名 +export function getPlanScenes() { + return getScenarioTypes(); +} +export function createScenarioPlan(data: any) { + return createPlan(data); +} +export function fetchPlanDetail(planId: string) { + return getPlanDetail(planId); +} +export function updateScenarioPlan(planId: string, data: any) { + return updatePlan(planId, data); +} diff --git a/nkebao/src/pages/scenarios/plan/new/index.module.scss b/nkebao/src/pages/scenarios/plan/new/index.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/nkebao/src/pages/scenarios/plan/new/index.tsx b/nkebao/src/pages/scenarios/plan/new/index.tsx index cf52b1047..e87397601 100644 --- a/nkebao/src/pages/scenarios/plan/new/index.tsx +++ b/nkebao/src/pages/scenarios/plan/new/index.tsx @@ -1,21 +1,20 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile"; -import { LeftOutline } from "antd-mobile-icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { LeftOutlined } from "@ant-design/icons"; +import { Button, Steps, message } from "antd"; import BasicSettings from "./steps/BasicSettings"; import FriendRequestSettings from "./steps/FriendRequestSettings"; import MessageSettings from "./steps/MessageSettings"; +import Layout from "@/components/Layout/Layout"; import { - getScenarioTypes, - createPlan, - updatePlan, - getPlanDetail, -} from "./page.api"; -import style from "./page.module.scss"; + getPlanScenes, + createScenarioPlan, + fetchPlanDetail, + PlanDetail, + updateScenarioPlan, +} from "./index.api"; -// 步骤定义 +// 步骤定义 - 只保留三个步骤 const steps = [ { id: 1, title: "步骤一", subtitle: "基础设置" }, { id: 2, title: "步骤二", subtitle: "好友申请设置" }, @@ -26,7 +25,7 @@ const steps = [ interface FormData { name: string; scenario: number; - posters: any[]; + posters: any[]; // 后续可替换为具体Poster类型 device: string[]; remarkType: string; greeting: string; @@ -39,8 +38,8 @@ interface FormData { addFriendInterval: number; } -const NewPlan: React.FC = () => { - const navigate = useNavigate(); +export default function NewPlan() { + const router = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [formData, setFormData] = useState({ name: "", @@ -64,57 +63,53 @@ const NewPlan: React.FC = () => { planId: string; }>(); const [isEdit, setIsEdit] = useState(false); - const [saving, setSaving] = useState(false); - useEffect(() => { loadData(); }, []); const loadData = async () => { setSceneLoading(true); - try { - // 获取场景类型 - const res = await getScenarioTypes(); - if (res?.data) { - setSceneList(res.data); - } - - if (planId) { - setIsEdit(true); - // 获取计划详情 - const detailRes = await getPlanDetail(planId); - if (detailRes.code === 200 && detailRes.data) { - const detail = detailRes.data; - setFormData((prev) => ({ - ...prev, - name: detail.name ?? "", - scenario: Number(detail.scenario) || 1, - posters: detail.posters ?? [], - device: detail.device ?? [], - remarkType: detail.remarkType ?? "phone", - greeting: detail.greeting ?? "", - addInterval: detail.addInterval ?? 1, - startTime: detail.startTime ?? "09:00", - endTime: detail.endTime ?? "18:00", - enabled: detail.enabled ?? true, - sceneId: Number(detail.scenario) || 1, - remarkFormat: detail.remarkFormat ?? "", - addFriendInterval: detail.addFriendInterval ?? 1, - })); - } - } else if (scenarioId) { + //获取场景类型 + getPlanScenes() + .then((data) => { + setSceneList(data || []); + }) + .catch((err) => { + message.error(err.message || "获取场景类型失败"); + }) + .finally(() => setSceneLoading(false)); + if (planId) { + setIsEdit(true); + //获取计划详情 + try { + const detail = await fetchPlanDetail(planId); setFormData((prev) => ({ ...prev, - scenario: Number(scenarioId) || 1, + name: detail.name ?? "", + scenario: Number(detail.scenario) || 1, + posters: detail.posters ?? [], + device: detail.device ?? [], + remarkType: detail.remarkType ?? "phone", + greeting: detail.greeting ?? "", + addInterval: detail.addInterval ?? 1, + startTime: detail.startTime ?? "09:00", + endTime: detail.endTime ?? "18:00", + enabled: detail.enabled ?? true, + sceneId: Number(detail.scenario) || 1, + remarkFormat: detail.remarkFormat ?? "", + addFriendInterval: detail.addFriendInterval ?? 1, + tips: detail.tips ?? "", + })); + } catch (err) { + message.error(err.message || "获取计划详情失败"); + } + } else { + if (scenarioId) { + setFormData((prev) => ({ + ...prev, + ...{ scenario: Number(scenarioId) || 1 }, })); } - } catch (error) { - Toast.show({ - content: "加载数据失败", - position: "top", - }); - } finally { - setSceneLoading(false); } }; @@ -125,52 +120,35 @@ const NewPlan: React.FC = () => { // 处理保存 const handleSave = async () => { - if (!formData.name.trim()) { - Toast.show({ - content: "请输入计划名称", - position: "top", - }); - return; - } - - setSaving(true); try { let result; if (isEdit && planId) { - // 编辑 + // 编辑:拼接后端需要的完整参数 const editData = { ...formData, id: Number(planId), planId: Number(planId), + // 兼容后端需要的字段 + // 你可以根据实际需要补充其它字段 }; - result = await updatePlan(planId, editData); + result = await updateScenarioPlan(planId, editData); } else { // 新建 - result = await createPlan(formData); - } - - if (result.code === 200) { - Toast.show({ - content: isEdit ? "计划已更新" : "获客计划已创建", - position: "top", - }); - const sceneItem = sceneList.find((v) => formData.scenario === v.id); - navigate( - `/scenarios/list/${formData.sceneId}/${sceneItem?.name || ""}` - ); - } else { - Toast.show({ - content: result.msg || "操作失败", - position: "top", - }); + result = await createScenarioPlan(formData); } + message.success(isEdit ? "计划已更新" : "获客计划已创建"); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + router(`/scenarios/list/${formData.sceneId}/${sceneItem.name}`); } catch (error) { - Toast.show({ - content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试", - position: "top", - }); - } finally { - setSaving(false); + message.error( + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : isEdit + ? "更新计划失败,请重试" + : "创建计划失败,请重试" + ); } }; @@ -194,6 +172,7 @@ const NewPlan: React.FC = () => { case 1: return ( { ); default: @@ -225,69 +203,37 @@ const NewPlan: React.FC = () => { } }; - if (sceneLoading) { - return ( - -
- {isEdit ? "编辑计划" : "新建计划"} -
- - } - footer={} - > -
- -
加载数据中...
-
-
- ); - } - return ( - {isEdit ? "编辑计划" : "新建计划"} + <> +
+
+
+
- } - right={ - - } - /> +
+
+ + {steps.map((step, idx) => ( + + ))} + +
+ } - footer={} > -
- {/* 步骤指示器 */} -
- - {steps.map((step) => ( - - ))} - -
- - {/* 步骤内容 */} -
{renderStepContent()}
-
+
{renderStepContent()}
); -}; - -export default NewPlan; +} diff --git a/nkebao/src/pages/scenarios/plan/new/page.api.ts b/nkebao/src/pages/scenarios/plan/new/page.api.ts deleted file mode 100644 index 9d1411bff..000000000 --- a/nkebao/src/pages/scenarios/plan/new/page.api.ts +++ /dev/null @@ -1,20 +0,0 @@ -import request from "@/api/request"; -// 获取场景类型列表 -export function getScenarioTypes() { - return request("/api/scenarios/types", undefined, "GET"); -} - -// 创建计划 -export function createPlan(data: any) { - return request("/api/scenarios/plans", data, "POST"); -} - -// 更新计划 -export function updatePlan(planId: string, data: any) { - return request(`/api/scenarios/plans/${planId}`, data, "PUT"); -} - -// 获取计划详情 -export function getPlanDetail(planId: string) { - return request(`/api/scenarios/plans/${planId}`, undefined, "GET"); -} diff --git a/nkebao/src/pages/scenarios/plan/new/page.module.scss b/nkebao/src/pages/scenarios/plan/new/page.module.scss deleted file mode 100644 index eaff99f16..000000000 --- a/nkebao/src/pages/scenarios/plan/new/page.module.scss +++ /dev/null @@ -1,43 +0,0 @@ -.new-plan-page { - background: #f5f5f5; - min-height: 100vh; -} - -.nav-title { - font-size: 18px; - font-weight: 600; - color: #333; -} - -.back-btn { - height: 32px; - width: 32px; - padding: 0; - border-radius: 50%; -} - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 60vh; - gap: 16px; -} - -.loading-text { - color: #666; - font-size: 14px; -} - -.steps-container { - background: white; - padding: 20px 16px; - margin-bottom: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.step-content { - flex: 1; - padding: 0 16px; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss deleted file mode 100644 index 5720527de..000000000 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.basic-settings { - padding: 16px 0; -} - -.form-card { - margin-bottom: 20px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.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-radius: 8px; - } - - .adm-selector { - border-radius: 8px; - } -} - -.time-input { - width: 120px; - border-radius: 8px; -} - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 40vh; - gap: 16px; -} - -.loading-text { - color: #666; - font-size: 14px; -} - -.actions { - padding: 20px 0; -} - -.next-btn { - width: 100%; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx index ef9bc1673..1f9895324 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx @@ -1,209 +1,858 @@ -import React, { useState, useEffect } from "react"; -import { - Form, - Input, - Selector, - Button, - SpinLoading, - Toast, - Card, - Space, -} from "antd-mobile"; -// import { getDevices, getPosters } from "./step.api"; -import style from "./BasicSettings.module.scss"; - -interface BasicSettingsProps { - formData: any; - onChange: (data: any) => void; - onNext: () => void; - sceneList: any[]; - sceneLoading: boolean; -} - -const BasicSettings: React.FC = ({ - formData, - onChange, - onNext, - sceneList, - sceneLoading, -}) => { - const [devices, setDevices] = useState([]); - const [posters, setPosters] = useState([]); - const [loading, setLoading] = useState(false); - - useEffect(() => { - loadData(); - }, []); - - const loadData = async () => { - setLoading(true); - try { - // // 获取设备列表 - // const devicesRes = await getDevices(); - // if (devicesRes?.data) { - // setDevices(devicesRes.data); - // } - // // 获取海报列表 - // const postersRes = await getPosters(); - // if (postersRes?.data) { - // setPosters(postersRes.data); - // } - } catch (error) { - Toast.show({ - content: "加载数据失败", - position: "top", - }); - } finally { - setLoading(false); - } - }; - - const handleNext = () => { - if (!formData.name.trim()) { - Toast.show({ - content: "请输入计划名称", - position: "top", - }); - return; - } - - if (!formData.scenario) { - Toast.show({ - content: "请选择场景类型", - position: "top", - }); - return; - } - - if (formData.device.length === 0) { - Toast.show({ - content: "请选择设备", - position: "top", - }); - return; - } - - onNext(); - }; - - if (loading || sceneLoading) { - return ( -
- -
加载数据中...
-
- ); - } - - return ( -
- -
- {/* 计划名称 */} - - onChange({ name: value })} - clearable - /> - - - {/* 场景类型 */} - - ({ - label: scene.name, - value: scene.id, - }))} - value={[formData.scenario]} - onChange={(value) => { - const selectedScene = sceneList.find( - (scene) => scene.id === value[0] - ); - onChange({ - scenario: value[0], - sceneId: value[0], - name: selectedScene?.name || "", - }); - }} - /> - - - {/* 选择设备 */} - - ({ - label: device.name, - value: device.id, - }))} - value={formData.device} - onChange={(value) => onChange({ device: value })} - multiple - /> - - - {/* 选择海报 */} - - ({ - label: poster.name, - value: poster.id, - }))} - value={formData.posters} - onChange={(value) => onChange({ posters: value })} - multiple - /> - - - {/* 工作时间 */} - - - onChange({ startTime: value })} - className={style["time-input"]} - /> - - onChange({ endTime: value })} - className={style["time-input"]} - /> - - - - {/* 添加间隔 */} - - - onChange({ addInterval: Number(value) || 1 }) - } - min={1} - max={60} - /> - -
-
- - {/* 操作按钮 */} -
- -
-
- ); -}; - -export default BasicSettings; +import React, { useState, useEffect, useRef } from "react"; +import { + Form, + Input, + Button, + Tag, + Switch, + // Upload, + Modal, + Alert, + Row, + Col, + message, +} from "antd"; +import { + PlusOutlined, + EyeOutlined, + CloseOutlined, + DownloadOutlined, + UploadOutlined, + CheckOutlined, +} from "@ant-design/icons"; +import { uploadFile } from "@/api/common"; + +interface BasicSettingsProps { + isEdit: boolean; + formData: any; + onChange: (data: any) => void; + onNext?: () => void; + sceneList: any[]; + sceneLoading: boolean; +} + +interface Account { + id: string; + nickname: string; + avatar: string; +} + +interface Material { + id: string; + name: string; + type: string; + preview: string; +} + +const posterTemplates = [ + { + id: "poster-1", + name: "点击领取", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif", + }, + { + id: "poster-2", + name: "点击合作", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif", + }, + { + id: "poster-3", + name: "点击咨询", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif", + }, + { + id: "poster-4", + name: "点击签到", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif", + }, + { + id: "poster-5", + name: "点击了解", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif", + }, + { + id: "poster-6", + name: "点击报名", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif", + }, +]; + +const generateRandomAccounts = (count: number): Account[] => { + return Array.from({ length: count }, (_, index) => ({ + id: `account-${index + 1}`, + nickname: `账号-${Math.random().toString(36).substring(2, 7)}`, + avatar: `/placeholder.svg?height=40&width=40&text=${index + 1}`, + })); +}; + +const generatePosterMaterials = (): Material[] => { + return posterTemplates.map((template) => ({ + id: template.id, + name: template.name, + type: "poster", + preview: template.preview, + })); +}; + +const BasicSettings: React.FC = ({ + isEdit, + formData, + onChange, + onNext, + sceneList, + sceneLoading, +}) => { + const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false); + const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [isPhoneSettingsOpen, setIsPhoneSettingsOpen] = useState(false); + const [accounts] = useState(generateRandomAccounts(50)); + const [materials] = useState(generatePosterMaterials()); + const [selectedAccounts, setSelectedAccounts] = useState( + formData.accounts?.length > 0 ? formData.accounts : [] + ); + const [selectedMaterials, setSelectedMaterials] = useState( + formData.materials?.length > 0 ? formData.materials : [] + ); + // showAllScenarios 默认为 true + const [showAllScenarios, setShowAllScenarios] = useState(true); + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); + const [importedTags, setImportedTags] = useState< + Array<{ + phone: string; + wechat: string; + source?: string; + orderAmount?: number; + orderDate?: string; + }> + >(formData.importedTags || []); + + // 自定义标签相关状态 + const [customTagInput, setCustomTagInput] = useState(""); + const [customTags, setCustomTags] = useState(formData.customTags || []); + const [tips, setTips] = useState(formData.tips || ""); + const [selectedScenarioTags, setSelectedScenarioTags] = useState( + formData.scenarioTags || [] + ); + + // 电话获客相关状态 + const [phoneSettings, setPhoneSettings] = useState({ + autoAdd: formData.phoneSettings?.autoAdd ?? true, + speechToText: formData.phoneSettings?.speechToText ?? true, + questionExtraction: formData.phoneSettings?.questionExtraction ?? true, + }); + + // 群设置相关状态 + const [weixinqunName, setWeixinqunName] = useState( + formData.weixinqunName || "" + ); + const [weixinqunNotice, setWeixinqunNotice] = useState( + formData.weixinqunNotice || "" + ); + + // 新增:自定义海报相关状态 + const [customPosters, setCustomPosters] = useState([]); + const [previewUrl, setPreviewUrl] = useState(null); + + // 新增:用于文件选择的ref + const uploadInputRef = useRef(null); + const uploadOrderInputRef = useRef(null); + + // 更新电话获客设置 + const handlePhoneSettingsUpdate = () => { + onChange({ ...formData, phoneSettings }); + setIsPhoneSettingsOpen(false); + }; + + // 处理标签选择 + const handleTagToggle = (tagId: string) => { + const newTags = selectedScenarioTags.includes(tagId) + ? selectedScenarioTags.filter((id: string) => id !== tagId) + : [...selectedScenarioTags, tagId]; + + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 处理通话类型选择 + const handleCallTypeChange = (type: string) => { + // setPhoneCallType(type) // This line was removed as per the edit hint. + onChange({ ...formData, phoneCallType: type }); + }; + + // 初始化时,如果没有选择场景,默认选择海报获客 + useEffect(() => { + if (!formData.scenario) { + onChange({ ...formData, scenario: "haibao" }); + } + + // 检查是否已经有上传的订单文件 + if (formData.orderFileUploaded) { + setOrderUploaded(true); + } + }, [formData, onChange]); + + useEffect(() => { + const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); + }, [isEdit]); + + useEffect(() => { + setTips(formData.tips || ""); + }, [formData.tips]); + + // 选中场景 + const handleScenarioSelect = (sceneId: number) => { + onChange({ ...formData, scenario: sceneId }); + }; + + // 选中/取消标签 + const handleScenarioTagToggle = (tag: string) => { + const newTags = selectedScenarioTags.includes(tag) + ? selectedScenarioTags.filter((t: string) => t !== tag) + : [...selectedScenarioTags, tag]; + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 添加自定义标签 + const handleAddCustomTag = () => { + if (!customTagInput.trim()) return; + const newTag = { + id: `custom-${Date.now()}`, + name: customTagInput.trim(), + }; + const updatedCustomTags = [...customTags, newTag]; + setCustomTags(updatedCustomTags); + setCustomTagInput(""); + onChange({ ...formData, customTags: updatedCustomTags }); + }; + + // 删除自定义标签 + const handleRemoveCustomTag = (tagId: string) => { + const updatedCustomTags = customTags.filter((tag: any) => tag.id !== tagId); + setCustomTags(updatedCustomTags); + onChange({ ...formData, customTags: updatedCustomTags }); + // 同时从选中标签中移除 + const updatedSelectedTags = selectedScenarioTags.filter( + (t: string) => t !== tagId + ); + setSelectedScenarioTags(updatedSelectedTags); + onChange({ + ...formData, + scenarioTags: updatedSelectedTags, + customTags: updatedCustomTags, + }); + }; + + // 新增:自定义上传图片 + const handleCustomPosterUpload = (urls: string[]) => { + if (urls && urls.length > 0) { + const newPoster: Material = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: urls[0], + }; + setCustomPosters((prev) => [...prev, newPoster]); + } + }; + + // 新增:删除自定义海报 + const handleRemoveCustomPoster = (id: string) => { + setCustomPosters((prev) => prev.filter((p) => p.id !== id)); + // 如果选中则取消选中 + if (selectedMaterials.some((m) => m.id === id)) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } + }; + + // 修改:选中/取消选中海报 + const handleMaterialSelect = (material: Material) => { + const isSelected = selectedMaterials.some((m) => m.id === material.id); + if (isSelected) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } else { + setSelectedMaterials([material]); + onChange({ ...formData, materials: [material] }); + } + }; + + // 移除已选素材 + const handleRemoveMaterial = (id: string) => { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + }; + + // 新增:全屏预览 + const handlePreviewImage = (url: string) => { + setPreviewUrl(url); + setIsPreviewOpen(true); + }; + + // 账号多选切换 + const handleAccountToggle = (account: Account) => { + const isSelected = selectedAccounts.some( + (a: Account) => a.id === account.id + ); + let newSelected; + if (isSelected) { + newSelected = selectedAccounts.filter( + (a: Account) => a.id !== account.id + ); + } else { + newSelected = [...selectedAccounts, account]; + } + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 移除已选账号 + const handleRemoveAccount = (id: string) => { + const newSelected = selectedAccounts.filter((a: Account) => a.id !== id); + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 处理文件导入 + const handleFileImport = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result as string; + const rows = content.split("\n").filter((row) => row.trim()); + const tags = rows.slice(1).map((row) => { + const [phone, wechat, source, orderAmount, orderDate] = + row.split(","); + return { + phone: phone?.trim(), + wechat: wechat?.trim(), + source: source?.trim(), + orderAmount: orderAmount ? Number(orderAmount) : undefined, + orderDate: orderDate?.trim(), + }; + }); + setImportedTags(tags); + onChange({ ...formData, importedTags: tags }); + } catch (error) { + // 可用 toast 提示 + } + }; + reader.readAsText(file); + } + }; + + // 下载模板 + const handleDownloadTemplate = () => { + const template = + "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"; + const blob = new Blob([template], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "订单导入模板.csv"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + // 修改订单表格上传逻辑,使用 uploadFile 公共方法 + const [orderUploaded, setOrderUploaded] = useState(false); + + const handleOrderFileUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (file) { + try { + await uploadFile(file); // 默认接口即可 + setOrderUploaded(true); + onChange({ ...formData, orderFileUploaded: true }); + // 可用 toast 或其它方式提示成功 + // alert('上传成功'); + } catch (err) { + // 可用 toast 或其它方式提示失败 + // alert('上传失败'); + } + event.target.value = ""; + } + }; + + // 账号弹窗关闭时清理搜索等状态 + const handleAccountDialogClose = () => { + setIsAccountDialogOpen(false); + // 可在此清理账号搜索等临时状态 + }; + // 素材弹窗关闭时清理搜索等状态 + const handleMaterialDialogClose = () => { + setIsMaterialDialogOpen(false); + // 可在此清理素材搜索等临时状态 + }; + // 订单导入弹窗关闭时清理文件输入等状态 + const handleImportDialogClose = () => { + setIsImportDialogOpen(false); + // 可在此清理文件输入等临时状态 + }; + // 电话获客弹窗关闭 + const handlePhoneSettingsDialogClose = () => { + setIsPhoneSettingsOpen(false); + }; + // 图片预览关闭 + const handleImagePreviewClose = () => { + setIsPreviewOpen(false); + }; + + // 当前选中的场景对象 + const currentScene = sceneList.find((s) => s.id === formData.scenario); + //打开订单 + const openOrder = + formData.scenario !== 2 ? { display: "none" } : { display: "block" }; + + const openPoster = + formData.scenario !== 1 ? { display: "none" } : { display: "block" }; + + return ( +
+ {/* 场景选择区块 */} + {sceneLoading ? ( +
加载中...
+ ) : ( + + {sceneList.map((scene) => ( + + + + ))} + + )} + + {/* 计划名称输入区 */} +
计划名称
+
+ + onChange({ ...formData, name: String(e.target.value) }) + } + placeholder="请输入计划名称" + /> +
+ +
获客标签(可多选)
+ {/* 标签选择区块 */} + {formData.scenario && ( +
+ {(currentScene?.scenarioTags || []).map((tag: string) => ( + handleScenarioTagToggle(tag)} + style={{ marginBottom: 4 }} + > + {tag} + + ))} + {/* 自定义标签 */} + {customTags.map((tag: any) => ( + handleScenarioTagToggle(tag.id)} + style={{ marginBottom: 4 }} + closable + onClose={() => handleRemoveCustomTag(tag.id)} + > + {tag.name} + + ))} +
+ )} + {/* 自定义标签输入区 */} +
+
+ setCustomTagInput(e.target.value)} + placeholder="添加自定义标签" + className="w-full" + /> +
+
+ +
+
+ + {/* 输入获客成功提示 */} +
+
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); + }} + placeholder="请输入获客成功提示" + className="w-full" + /> +
+
+ + {/* 选素材 */} +
+
选择海报
+
+ {[...materials, ...customPosters].map((material) => { + const isSelected = selectedMaterials.some( + (m) => m.id === material.id + ); + const isCustom = material.id.startsWith("custom-"); + return ( +
handleMaterialSelect(material)} + > + {/* 预览按钮:自定义海报在左上,内置海报在右上 */} + + {/* 删除自定义海报按钮 */} + {isCustom && ( + + )} + {material.name} +
+ {material.name} +
+
+ ); + })} + {/* 添加海报卡片 */} +
uploadInputRef.current?.click()} + > + + + + 添加海报 + { + const file = e.target.files?.[0]; + if (file) { + // 直接上传 + try { + const url = await uploadFile(file); + const newPoster = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: url, + }; + setCustomPosters((prev) => [...prev, newPoster]); + } catch (err) { + // 可加toast提示 + } + e.target.value = ""; + } + }} + /> +
+
+ {/* 全屏图片预览 */} + { + setIsPreviewOpen(false); + setPreviewUrl(null); + }} + footer={null} + width={800} + > + {previewUrl && ( + Preview + )} + +
+ {/* 订单导入区块优化 */} +
+
订单表格上传
+
+ + +
+
+ 支持 CSV、Excel 格式,上传后将文件保存到服务器 +
+
+ {/* 电话获客设置区块,仅在选择电话获客场景时显示 */} + {formData.scenario === 5 && ( +
+
+
+ 电话获客设置 +
+
+
+ 自动加好友 + + setPhoneSettings((s) => ({ ...s, autoAdd: v })) + } + /> +
+
+ 语音转文字 + + setPhoneSettings((s) => ({ ...s, speechToText: v })) + } + /> +
+
+ 问题提取 + + setPhoneSettings((s) => ({ ...s, questionExtraction: v })) + } + /> +
+
+
+
+ )} + {/* 微信群设置区块,仅在选择微信群场景时显示 */} + {formData.scenario === 7 && ( +
+
+ onChange({ ...formData, weixinqunName })} + /> +
+
+ onChange({ ...formData, weixinqunNotice })} + /> +
+
+ )} + +
+ 是否启用 + onChange({ ...formData, enabled: value })} + /> +
+ +
+ ); +}; + +export default BasicSettings; diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss deleted file mode 100644 index 67047ef14..000000000 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss +++ /dev/null @@ -1,56 +0,0 @@ -.friend-request-settings { - padding: 16px 0; -} - -.form-card { - margin-bottom: 20px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.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-radius: 8px; - } - - .adm-selector { - border-radius: 8px; - } - - .adm-text-area { - border-radius: 8px; - } -} - -.actions { - padding: 20px 0; -} - -.prev-btn { - flex: 1; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} - -.next-btn { - flex: 1; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx index fcb0bb63b..f76e0fb33 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -1,113 +1,259 @@ -import React from "react"; -import { - Form, - Input, - Selector, - Button, - Card, - Space, - TextArea, -} from "antd-mobile"; -import style from "./FriendRequestSettings.module.scss"; - -interface FriendRequestSettingsProps { - formData: any; - onChange: (data: any) => void; - onNext: () => void; - onPrev: () => void; -} - -const FriendRequestSettings: React.FC = ({ - formData, - onChange, - onNext, - onPrev, -}) => { - const remarkTypeOptions = [ - { label: "手机号", value: "phone" }, - { label: "微信号", value: "wechat" }, - { label: "QQ号", value: "qq" }, - { label: "自定义", value: "custom" }, - ]; - - const handleNext = () => { - if (!formData.greeting.trim()) { - // 可以添加验证逻辑 - } - onNext(); - }; - - return ( -
- -
- {/* 备注类型 */} - - onChange({ remarkType: value[0] })} - /> - - - {/* 备注格式 */} - {formData.remarkType === "custom" && ( - - onChange({ remarkFormat: value })} - clearable - /> - - )} - - {/* 打招呼消息 */} - -