diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ca6fe068 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/Cunkebao/components/ExcelImporter.tsx b/Cunkebao/components/ExcelImporter.tsx index d0b55979..380ab859 100644 --- a/Cunkebao/components/ExcelImporter.tsx +++ b/Cunkebao/components/ExcelImporter.tsx @@ -27,7 +27,10 @@ interface ExcelImporterProps { onReset?: () => void; } -export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) { +export default function ExcelImporter({ + onImport, + onReset, +}: ExcelImporterProps) { const [parsedData, setParsedData] = useState([]); const [error, setError] = useState(null); const [fileName, setFileName] = useState(""); @@ -51,62 +54,64 @@ export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) setParsedData([]); setDetectedColumns({}); setIsProcessing(true); - + const file = files[0]; setFileName(file.name); - + const reader = new FileReader(); reader.onload = (event) => { try { const data = event.target?.result; - + if (!data) { setError("文件内容为空,请检查文件是否有效"); setIsProcessing(false); return; } - + // 尝试将读取的二进制数据解析为Excel工作簿 let workbook; try { - workbook = XLSX.read(data, { type: 'binary' }); + workbook = XLSX.read(data, { type: "binary" }); } catch (parseErr) { console.error("解析Excel内容失败:", parseErr); - setError("无法解析文件内容,请确保上传的是有效的Excel文件(.xlsx或.xls格式)"); + setError( + "无法解析文件内容,请确保上传的是有效的Excel文件(.xlsx或.xls格式)" + ); setIsProcessing(false); return; } - + if (!workbook.SheetNames || workbook.SheetNames.length === 0) { setError("Excel文件中没有找到工作表"); setIsProcessing(false); return; } - + // 取第一个工作表 const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; - + if (!worksheet) { setError(`无法读取工作表 "${sheetName}",请检查文件是否损坏`); setIsProcessing(false); return; } - + // 将工作表转换为JSON const jsonData = XLSX.utils.sheet_to_json(worksheet); - + if (!jsonData || jsonData.length === 0) { setError("Excel 文件中没有数据"); setIsProcessing(false); return; } - + // 查找栏位对应的列 let mobileColumn: string | null = null; let fromColumn: string | null = null; let aliasColumn: string | null = null; - + // 遍历第一行查找栏位 const firstRow = jsonData[0] as Record; if (!firstRow) { @@ -114,88 +119,116 @@ export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) setIsProcessing(false); return; } - + for (const key in firstRow) { if (!firstRow[key]) continue; // 跳过空值 - + const value = String(firstRow[key]).toLowerCase(); - + // 扩展匹配列表,提高识别成功率 - if (value.includes("手机") || value.includes("电话") || value.includes("mobile") || - value.includes("phone") || value.includes("tel") || value.includes("cell")) { + if ( + value.includes("手机") || + value.includes("电话") || + value.includes("mobile") || + value.includes("phone") || + value.includes("tel") || + value.includes("cell") + ) { mobileColumn = key; - } else if (value.includes("来源") || value.includes("source") || value.includes("from") || - value.includes("channel") || value.includes("渠道")) { + } else if ( + value.includes("来源") || + value.includes("source") || + value.includes("from") || + value.includes("channel") || + value.includes("渠道") + ) { fromColumn = key; - } else if (value.includes("微信") || value.includes("alias") || value.includes("wechat") || - value.includes("wx") || value.includes("id") || value.includes("账号")) { + } else if ( + value.includes("微信") || + value.includes("alias") || + value.includes("wechat") || + value.includes("wx") || + value.includes("id") || + value.includes("账号") + ) { aliasColumn = key; } } - + // 保存检测到的列名 if (mobileColumn && firstRow[mobileColumn]) { - setDetectedColumns(prev => ({ ...prev, mobile: String(firstRow[mobileColumn]) })); + setDetectedColumns((prev) => ({ + ...prev, + mobile: String(firstRow[mobileColumn]), + })); } if (fromColumn && firstRow[fromColumn]) { - setDetectedColumns(prev => ({ ...prev, from: String(firstRow[fromColumn]) })); + setDetectedColumns((prev) => ({ + ...prev, + from: String(firstRow[fromColumn]), + })); } if (aliasColumn && firstRow[aliasColumn]) { - setDetectedColumns(prev => ({ ...prev, alias: String(firstRow[aliasColumn]) })); + setDetectedColumns((prev) => ({ + ...prev, + alias: String(firstRow[aliasColumn]), + })); } - + if (!mobileColumn) { - setError("未找到手机号码栏位,请确保Excel中包含手机、电话、mobile或phone等栏位名称"); + setError( + "未找到手机号码栏位,请确保Excel中包含手机、电话、mobile或phone等栏位名称" + ); setIsProcessing(false); return; } - + // 取第二行开始的数据(跳过标题行) const importedData: ContactData[] = []; for (let i = 1; i < jsonData.length; i++) { const row = jsonData[i] as Record; - + // 确保手机号存在且不为空 if (!row || !row[mobileColumn]) continue; - + // 处理手机号,去掉非数字字符 let mobileValue = row[mobileColumn]; let mobileNumber: number; - - if (typeof mobileValue === 'number') { + + if (typeof mobileValue === "number") { mobileNumber = mobileValue; } else { // 如果是字符串,去掉非数字字符 - const mobileStr = String(mobileValue).trim().replace(/\D/g, ''); + const mobileStr = String(mobileValue).trim().replace(/\D/g, ""); if (!mobileStr) continue; // 如果手机号为空,跳过该行 mobileNumber = Number(mobileStr); if (isNaN(mobileNumber)) continue; // 如果转换后不是数字,跳过该行 } - + // 构建数据对象 const contact: ContactData = { - mobile: mobileNumber + mobile: mobileNumber, }; - + // 添加来源字段(如果存在) if (fromColumn && row[fromColumn]) { contact.from = String(row[fromColumn]).trim(); } - + // 添加微信号字段(如果存在) if (aliasColumn && row[aliasColumn]) { contact.alias = String(row[aliasColumn]).trim(); } - + importedData.push(contact); } - + if (importedData.length === 0) { setError("未找到有效数据,请确保Excel中至少有一行有效的手机号码"); setIsProcessing(false); return; } - + setParsedData(importedData); setIsProcessing(false); } catch (err) { @@ -204,12 +237,12 @@ export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) setIsProcessing(false); } }; - + reader.onerror = () => { setError("读取文件时出错,请重试"); setIsProcessing(false); }; - + reader.readAsBinaryString(file); }; @@ -245,29 +278,27 @@ export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) disabled={isProcessing} /> {fileName && ( -

- 当前文件: {fileName} -

+

当前文件: {fileName}

)}
请确保Excel文件包含以下列: 手机号码(必需)、来源(可选)、微信号(可选)
- + {isProcessing && (

正在处理Excel文件...

)} - + {error && ( {error} )} - + {isImportSuccessful && ( @@ -275,30 +306,40 @@ export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) )} - + {parsedData.length > 0 && !isImportSuccessful && (

已解析 {parsedData.length} 条有效数据,点击下方按钮确认导入。

- + {Object.keys(detectedColumns).length > 0 && (

检测到的列名:

    - {detectedColumns.mobile &&
  • 手机号: {detectedColumns.mobile}
  • } - {detectedColumns.from &&
  • 来源: {detectedColumns.from}
  • } - {detectedColumns.alias &&
  • 微信号: {detectedColumns.alias}
  • } + {detectedColumns.mobile && ( +
  • 手机号: {detectedColumns.mobile}
  • + )} + {detectedColumns.from && ( +
  • 来源: {detectedColumns.from}
  • + )} + {detectedColumns.alias && ( +
  • 微信号: {detectedColumns.alias}
  • + )}
)} - +

数据示例:

{JSON.stringify(parsedData.slice(0, 3), null, 2)}
- {parsedData.length > 3 &&

...共 {parsedData.length} 条

} + {parsedData.length > 3 && ( +

+ ...共 {parsedData.length} 条 +

+ )}
@@ -317,17 +358,19 @@ export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps)
)} - +
- -
-
@@ -111,9 +133,9 @@ export function ContentLibrarySelectionDialog({ 已选择 {tempSelected.length} 个内容库
-
@@ -171,11 +200,11 @@ export function ContentLibrarySelectionDialog({ 取消 ); -} \ No newline at end of file +} diff --git a/nkebao/src/components/DeviceSelection.tsx b/nkebao/src/components/DeviceSelection.tsx index 7f341d76..79fcdca8 100644 --- a/nkebao/src/components/DeviceSelection.tsx +++ b/nkebao/src/components/DeviceSelection.tsx @@ -1,11 +1,16 @@ -import React, { useState, useEffect } from 'react'; -import { Search } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { fetchDeviceList } from '@/api/devices'; +import React, { useState, useEffect } from "react"; +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { fetchDeviceList } from "@/api/devices"; // 设备选择项接口 interface DeviceSelectionItem { @@ -13,7 +18,7 @@ interface DeviceSelectionItem { name: string; imei: string; wechatId: string; - status: 'online' | 'offline'; + status: "online" | "offline"; } // 组件属性接口 @@ -24,16 +29,16 @@ interface DeviceSelectionProps { className?: string; } -export default function DeviceSelection({ - selectedDevices, - onSelect, +export default function DeviceSelection({ + selectedDevices, + onSelect, placeholder = "选择设备", - className = "" + className = "", }: DeviceSelectionProps) { const [dialogOpen, setDialogOpen] = useState(false); const [devices, setDevices] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(false); // 当弹窗打开时获取设备列表 @@ -49,16 +54,18 @@ export default function DeviceSelection({ try { const res = await fetchDeviceList(1, 100); if (res && res.data && Array.isArray(res.data.list)) { - setDevices(res.data.list.map(d => ({ - id: d.id?.toString() || '', - name: d.memo || d.imei || '', - imei: d.imei || '', - wechatId: d.wechatId || '', - status: d.alive === 1 ? 'online' : 'offline', - }))); + setDevices( + res.data.list.map((d) => ({ + id: d.id?.toString() || "", + name: d.memo || d.imei || "", + imei: d.imei || "", + wechatId: d.wechatId || "", + status: d.alive === 1 ? "online" : "offline", + })) + ); } } catch (error) { - console.error('获取设备列表失败:', error); + console.error("获取设备列表失败:", error); } finally { setLoading(false); } @@ -72,9 +79,9 @@ export default function DeviceSelection({ device.wechatId.toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = - statusFilter === 'all' || - (statusFilter === 'online' && device.status === 'online') || - (statusFilter === 'offline' && device.status === 'offline'); + statusFilter === "all" || + (statusFilter === "online" && device.status === "online") || + (statusFilter === "offline" && device.status === "offline"); return matchesSearch && matchesStatus; }); @@ -82,7 +89,7 @@ export default function DeviceSelection({ // 处理设备选择 const handleDeviceToggle = (deviceId: string) => { if (selectedDevices.includes(deviceId)) { - onSelect(selectedDevices.filter(id => id !== deviceId)); + onSelect(selectedDevices.filter((id) => id !== deviceId)); } else { onSelect([...selectedDevices, deviceId]); } @@ -90,7 +97,7 @@ export default function DeviceSelection({ // 获取显示文本 const getDisplayText = () => { - if (selectedDevices.length === 0) return ''; + if (selectedDevices.length === 0) return ""; return `已选择 ${selectedDevices.length} 个设备`; }; @@ -108,10 +115,9 @@ export default function DeviceSelection({ /> - {/* 设备选择弹窗 */} - + 选择设备 @@ -128,7 +134,7 @@ export default function DeviceSelection({ setStatusFilter(e.target.value)} className="w-32 px-3 py-2 border border-gray-300 rounded-md text-sm" > @@ -119,8 +134,17 @@ export function DeviceSelectionDialog({ - @@ -149,17 +173,23 @@ export function DeviceSelectionDialog({
{device.name} - - {device.status === 'online' ? '在线' : '离线'} + + {device.status === "online" ? "在线" : "离线"}
IMEI: {device.imei}
-
微信号: {device.wxid||'-'}
-
昵称: {device.nickname||'-'}
+
微信号: {device.wxid || "-"}
+
昵称: {device.nickname || "-"}
{device.usedInPlans > 0 && ( -
已用于 {device.usedInPlans} 个计划
+
+ 已用于 {device.usedInPlans} 个计划 +
)}
@@ -177,11 +207,12 @@ export function DeviceSelectionDialog({ 取消
); -} \ No newline at end of file +} diff --git a/nkebao/src/components/FriendSelection.tsx b/nkebao/src/components/FriendSelection.tsx index 6e976dc7..a23584f7 100644 --- a/nkebao/src/components/FriendSelection.tsx +++ b/nkebao/src/components/FriendSelection.tsx @@ -1,10 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import { Search, X } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { get } from '@/api/request'; +import React, { useState, useEffect } from "react"; +import { Search, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { get } from "@/api/request"; // 微信好友接口类型 interface WechatFriend { @@ -42,18 +47,20 @@ const fetchFriendsList = async (params: { if (params.deviceIds && params.deviceIds.length === 0) { return { code: 200, - msg: 'success', + msg: "success", data: { list: [], total: 0, page: params.page, - limit: params.limit - } + limit: params.limit, + }, }; } - - const deviceIdsParam = params?.deviceIds?.join(',') || '' - return get(`/v1/friend?page=${ params.page}&limit=${params.limit}&deviceIds=${deviceIdsParam}`); + + const deviceIdsParam = params?.deviceIds?.join(",") || ""; + return get( + `/v1/friend?page=${params.page}&limit=${params.limit}&deviceIds=${deviceIdsParam}` + ); }; // 组件属性接口 @@ -67,18 +74,18 @@ interface FriendSelectionProps { className?: string; } -export default function FriendSelection({ - selectedFriends, - onSelect, +export default function FriendSelection({ + selectedFriends, + onSelect, onSelectDetail, deviceIds = [], enableDeviceFilter = true, placeholder = "选择微信好友", - className = "" + className = "", }: FriendSelectionProps) { const [dialogOpen, setDialogOpen] = useState(false); const [friends, setFriends] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalFriends, setTotalFriends] = useState(0); @@ -116,49 +123,52 @@ export default function FriendSelection({ } else { res = await fetchFriendsList({ page, limit: 20 }); } - + if (res && res.code === 200 && res.data) { - setFriends(res.data.list.map((friend) => ({ - id: friend.id?.toString() || '', - nickname: friend.nickname || '', - wechatId: friend.wechatId || '', - avatar: friend.avatar || '', - customer: friend.customer || '', - }))); + setFriends( + res.data.list.map((friend) => ({ + id: friend.id?.toString() || "", + nickname: friend.nickname || "", + wechatId: friend.wechatId || "", + avatar: friend.avatar || "", + customer: friend.customer || "", + })) + ); setTotalFriends(res.data.total || 0); setTotalPages(Math.ceil((res.data.total || 0) / 20)); } } catch (error) { - console.error('获取好友列表失败:', error); + console.error("获取好友列表失败:", error); } finally { setLoading(false); } }; // 过滤好友 - const filteredFriends = friends.filter(friend => - friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || - friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredFriends = friends.filter( + (friend) => + friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || + friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) ); // 处理好友选择 const handleFriendToggle = (friendId: string) => { let newIds: string[]; if (selectedFriends.includes(friendId)) { - newIds = selectedFriends.filter(id => id !== friendId); + newIds = selectedFriends.filter((id) => id !== friendId); } else { newIds = [...selectedFriends, friendId]; } onSelect(newIds); if (onSelectDetail) { - const selectedObjs = friends.filter(f => newIds.includes(f.id)); + const selectedObjs = friends.filter((f) => newIds.includes(f.id)); onSelectDetail(selectedObjs); } }; // 获取显示文本 const getDisplayText = () => { - if (selectedFriends.length === 0) return ''; + if (selectedFriends.length === 0) return ""; return `已选择 ${selectedFriends.length} 个好友`; }; @@ -171,8 +181,19 @@ export default function FriendSelection({ {/* 输入框 */}
- - + + - +
- 选择微信好友 - + + 选择微信好友 + +
- @@ -223,7 +246,13 @@ export default function FriendSelection({ onClick={() => handleFriendToggle(friend.id)} >
-
+
{selectedFriends.includes(friend.id) && (
)} @@ -232,16 +261,24 @@ export default function FriendSelection({
{friend.avatar ? ( - {friend.nickname} + {friend.nickname} ) : ( friend.nickname.charAt(0) )}
{friend.nickname}
-
微信ID: {friend.wechatId}
+
+ 微信ID: {friend.wechatId} +
{friend.customer && ( -
归属客户: {friend.customer}
+
+ 归属客户: {friend.customer} +
)}
@@ -251,7 +288,7 @@ export default function FriendSelection({ ) : (
- {deviceIds.length === 0 ? '请先选择设备' : '没有找到好友'} + {deviceIds.length === 0 ? "请先选择设备" : "没有找到好友"}
)} @@ -271,11 +308,15 @@ export default function FriendSelection({ > < - {currentPage} / {totalPages} + + {currentPage} / {totalPages} +
- -
@@ -296,4 +344,4 @@ export default function FriendSelection({ ); -} \ No newline at end of file +} diff --git a/nkebao/src/components/GroupSelection.tsx b/nkebao/src/components/GroupSelection.tsx index 2ee6b419..534cf146 100644 --- a/nkebao/src/components/GroupSelection.tsx +++ b/nkebao/src/components/GroupSelection.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect } from 'react'; -import { Search, X } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; -import { get } from '@/api/request'; +import React, { useState, useEffect } from "react"; +import { Search, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { get } from "@/api/request"; // 群组接口类型 interface WechatGroup { @@ -36,8 +36,13 @@ interface GroupsResponse { }; } -const fetchGroupsList = async (params: { page: number; limit: number; }): Promise => { - return get(`/v1/chatroom?page=${params.page}&limit=${params.limit}`); +const fetchGroupsList = async (params: { + page: number; + limit: number; +}): Promise => { + return get( + `/v1/chatroom?page=${params.page}&limit=${params.limit}` + ); }; interface GroupSelectionProps { @@ -52,12 +57,12 @@ export default function GroupSelection({ selectedGroups, onSelect, onSelectDetail, - placeholder = '选择群聊', - className = '' + placeholder = "选择群聊", + className = "", }: GroupSelectionProps) { const [dialogOpen, setDialogOpen] = useState(false); const [groups, setGroups] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalGroups, setTotalGroups] = useState(0); @@ -84,49 +89,52 @@ export default function GroupSelection({ try { const res = await fetchGroupsList({ page, limit: 20 }); if (res && res.code === 200 && res.data) { - setGroups(res.data.list.map((group) => ({ - id: group.id?.toString() || '', - chatroomId: group.chatroomId || '', - name: group.name || '', - avatar: group.avatar || '', - ownerWechatId: group.ownerWechatId || '', - ownerNickname: group.ownerNickname || '', - ownerAvatar: group.ownerAvatar || '', - }))); + setGroups( + res.data.list.map((group) => ({ + id: group.id?.toString() || "", + chatroomId: group.chatroomId || "", + name: group.name || "", + avatar: group.avatar || "", + ownerWechatId: group.ownerWechatId || "", + ownerNickname: group.ownerNickname || "", + ownerAvatar: group.ownerAvatar || "", + })) + ); setTotalGroups(res.data.total || 0); setTotalPages(Math.ceil((res.data.total || 0) / 20)); } } catch (error) { - console.error('获取群组列表失败:', error); + console.error("获取群组列表失败:", error); } finally { setLoading(false); } }; // 过滤群组 - const filteredGroups = groups.filter(group => - group.name.toLowerCase().includes(searchQuery.toLowerCase()) || - group.chatroomId.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredGroups = groups.filter( + (group) => + group.name.toLowerCase().includes(searchQuery.toLowerCase()) || + group.chatroomId.toLowerCase().includes(searchQuery.toLowerCase()) ); // 处理群组选择 const handleGroupToggle = (groupId: string) => { let newIds: string[]; if (selectedGroups.includes(groupId)) { - newIds = selectedGroups.filter(id => id !== groupId); + newIds = selectedGroups.filter((id) => id !== groupId); } else { newIds = [...selectedGroups, groupId]; } onSelect(newIds); if (onSelectDetail) { - const selectedObjs = groups.filter(g => newIds.includes(g.id)); + const selectedObjs = groups.filter((g) => newIds.includes(g.id)); onSelectDetail(selectedObjs); } }; // 获取显示文本 const getDisplayText = () => { - if (selectedGroups.length === 0) return ''; + if (selectedGroups.length === 0) return ""; return `已选择 ${selectedGroups.length} 个群聊`; }; @@ -139,8 +147,19 @@ export default function GroupSelection({ {/* 输入框 */}
- - + + - +
- 选择群聊 + + 选择群聊 +
setSearchQuery('')} + onClick={() => setSearchQuery("")} > @@ -190,7 +211,13 @@ export default function GroupSelection({ onClick={() => handleGroupToggle(group.id)} >
-
+
{selectedGroups.includes(group.id) && (
)} @@ -199,16 +226,24 @@ export default function GroupSelection({
{group.avatar ? ( - {group.name} + {group.name} ) : ( group.name.charAt(0) )}
{group.name}
-
群ID: {group.chatroomId}
+
+ 群ID: {group.chatroomId} +
{group.ownerNickname && ( -
群主: {group.ownerNickname}
+
+ 群主: {group.ownerNickname} +
)}
@@ -236,11 +271,15 @@ export default function GroupSelection({ > < - {currentPage} / {totalPages} + + {currentPage} / {totalPages} +
- -
@@ -261,4 +307,4 @@ export default function GroupSelection({ ); -} \ No newline at end of file +} diff --git a/nkebao/src/components/Layout.tsx b/nkebao/src/components/Layout.tsx index 5ea387ef..1f4a1f6b 100644 --- a/nkebao/src/components/Layout.tsx +++ b/nkebao/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface LayoutProps { loading?: boolean; @@ -7,29 +7,19 @@ interface LayoutProps { footer?: React.ReactNode; } -const Layout: React.FC = ({ - loading, - children, - header, - footer +const Layout: React.FC = ({ + loading, + children, + header, + footer, }) => { return (
- {header && ( -
- {header} -
- )} -
- {children} -
- {footer && ( -
- {footer} -
- )} + {header &&
{header}
} +
{children}
+ {footer &&
{footer}
}
); }; -export default Layout; \ No newline at end of file +export default Layout; diff --git a/nkebao/src/components/ui/dialog.tsx b/nkebao/src/components/ui/dialog.tsx index 5238163b..6e20ba64 100644 --- a/nkebao/src/components/ui/dialog.tsx +++ b/nkebao/src/components/ui/dialog.tsx @@ -1,18 +1,18 @@ -"use client" +"use client"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; -import { cn } from "@/utils" +import { cn } from "@/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -22,12 +22,12 @@ const DialogOverlay = React.forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className, + className )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -38,8 +38,8 @@ const DialogContent = React.forwardRef< @@ -50,18 +50,36 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; -const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; -const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -69,19 +87,26 @@ const DialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -94,4 +119,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/nkebao/src/pages/scenarios/ScenarioDetail.tsx b/nkebao/src/pages/scenarios/ScenarioList.tsx similarity index 52% rename from nkebao/src/pages/scenarios/ScenarioDetail.tsx rename to nkebao/src/pages/scenarios/ScenarioList.tsx index 2059daeb..6c110961 100644 --- a/nkebao/src/pages/scenarios/ScenarioDetail.tsx +++ b/nkebao/src/pages/scenarios/ScenarioList.tsx @@ -1,12 +1,32 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; -import PageHeader from '@/components/PageHeader'; -import Layout from '@/components/Layout'; -import BottomNav from '@/components/BottomNav'; -import { Plus, Users, TrendingUp, Calendar, Copy, Trash2, Play, Pause, Settings, Loader2, Code } from 'lucide-react'; -import { fetchPlanList, fetchPlanDetail, copyPlan, deletePlan, type Task } from '@/api/scenarios'; -import { useToast } from '@/components/ui/toast'; -import '@/components/Layout.css'; +import React, { useEffect, useState, useCallback } from "react"; +import { useParams, useNavigate, useSearchParams } from "react-router-dom"; +import PageHeader from "@/components/PageHeader"; +import Layout from "@/components/Layout"; +import BottomNav from "@/components/BottomNav"; +import { + Plus, + Users, + Calendar, + Copy, + Trash2, + Edit, + Settings, + Loader2, + Code, + Search, + RefreshCw, +} from "lucide-react"; +import { + fetchPlanList, + fetchPlanDetail, + copyPlan, + deletePlan, + type Task, +} from "@/api/scenarios"; +import { useToast } from "@/components/ui/toast"; +import "@/components/Layout.css"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; interface ScenarioData { id: string; @@ -26,20 +46,25 @@ interface ApiSettings { } export default function ScenarioDetail() { - const { scenarioId } = useParams<{ scenarioId: string }>(); + const { scenarioId, scenarioName } = useParams<{ + scenarioId: string; + scenarioName: string; + }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { toast } = useToast(); const [scenario, setScenario] = useState(null); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const [showApiDialog, setShowApiDialog] = useState(false); const [currentApiSettings, setCurrentApiSettings] = useState({ - apiKey: '', - webhookUrl: '', - taskId: '', + apiKey: "", + webhookUrl: "", + taskId: "", }); + const [searchTerm, setSearchTerm] = useState(""); + const [loadingTasks, setLoadingTasks] = useState(false); // 获取渠道中文名称 const getChannelName = (channel: string) => { @@ -61,61 +86,61 @@ export default function ScenarioDetail() { // 获取场景描述 const getScenarioDescription = (channel: string) => { const descriptions: Record = { - douyin: '通过抖音平台进行精准获客,利用短视频内容吸引目标用户', - xiaohongshu: '利用小红书平台进行内容营销获客,通过优质内容建立品牌形象', - gongzhonghao: '通过微信公众号进行获客,建立私域流量池', - haibao: '通过海报分享进行获客,快速传播品牌信息', - phone: '通过电话营销进行获客,直接与客户沟通', - weixinqun: '通过微信群进行获客,利用社交裂变效应', - payment: '通过付款码进行获客,便捷的支付方式', - api: '通过API接口进行获客,支持第三方系统集成', + douyin: "通过抖音平台进行精准获客,利用短视频内容吸引目标用户", + xiaohongshu: "利用小红书平台进行内容营销获客,通过优质内容建立品牌形象", + gongzhonghao: "通过微信公众号进行获客,建立私域流量池", + haibao: "通过海报分享进行获客,快速传播品牌信息", + phone: "通过电话营销进行获客,直接与客户沟通", + weixinqun: "通过微信群进行获客,利用社交裂变效应", + payment: "通过付款码进行获客,便捷的支付方式", + api: "通过API接口进行获客,支持第三方系统集成", }; - return descriptions[channel] || '通过该平台进行获客'; + return descriptions[channel] || "通过该平台进行获客"; }; useEffect(() => { const fetchScenarioData = async () => { if (!scenarioId) return; - + setLoading(true); - setError(''); - + setError(""); + try { // 获取计划列表 const response = await fetchPlanList(scenarioId, 1, 20); - + // 设置计划列表(可能为空) if (response && response.data && response.data.list) { setTasks(response.data.list); } else { setTasks([]); } - + // 构建场景数据(无论是否有计划都要创建) const scenarioData: ScenarioData = { id: scenarioId, - name: getScenarioName(), - image: '', // 可以根据需要设置图片 + name: scenarioName || "", + image: "", // 可以根据需要设置图片 description: getScenarioDescription(scenarioId), totalPlans: response?.data?.list?.length || 0, totalCustomers: 0, // 移除统计 todayCustomers: 0, // 移除统计 - growth: '', // 移除增长 + growth: "", // 移除增长 }; setScenario(scenarioData); } catch (error) { - console.error('获取场景数据失败:', error); + console.error("获取场景数据失败:", error); // 即使API失败也要创建基本的场景数据 const scenarioData: ScenarioData = { id: scenarioId, name: getScenarioName(), - image: '', + image: "", description: getScenarioDescription(scenarioId), totalPlans: 0, totalCustomers: 0, todayCustomers: 0, - growth: '', + growth: "", }; setScenario(scenarioData); setTasks([]); @@ -130,28 +155,31 @@ export default function ScenarioDetail() { // 获取场景名称 - 优先使用URL查询参数,其次使用映射 const getScenarioName = useCallback(() => { // 优先使用URL查询参数中的name - const urlName = searchParams.get('name'); + const urlName = searchParams.get("name"); if (urlName) { return urlName; } - + // 如果没有URL参数,使用映射 - return getChannelName(scenarioId || ''); + return getChannelName(scenarioId || ""); }, [searchParams, scenarioId]); // 更新场景数据中的名称 useEffect(() => { - setScenario(prev => prev ? { - ...prev, - name: (() => { - const urlName = searchParams.get('name'); - if (urlName) return urlName; - return getChannelName(scenarioId || ''); - })() - } : null); + setScenario((prev) => + prev + ? { + ...prev, + name: (() => { + const urlName = searchParams.get("name"); + if (urlName) return urlName; + return getChannelName(scenarioId || ""); + })(), + } + : null + ); }, [searchParams, scenarioId]); - const handleCopyPlan = async (taskId: string) => { const taskToCopy = tasks.find((task) => task.id === taskId); if (!taskToCopy) return; @@ -160,24 +188,28 @@ export default function ScenarioDetail() { const response = await copyPlan(taskId); if (response && response.code === 200) { toast({ - title: '计划已复制', + title: "计划已复制", description: `已成功复制"${taskToCopy.name}"`, }); - + // 重新加载数据 const refreshResponse = await fetchPlanList(scenarioId!, 1, 20); - if (refreshResponse && refreshResponse.code === 200 && refreshResponse.data) { + if ( + refreshResponse && + refreshResponse.code === 200 && + refreshResponse.data + ) { setTasks(refreshResponse.data.list); } } else { - throw new Error(response?.msg || '复制失败'); + throw new Error(response?.msg || "复制失败"); } } catch (error) { - console.error('复制计划失败:', error); + console.error("复制计划失败:", error); toast({ - title: '复制失败', - description: error instanceof Error ? error.message : '复制计划失败', - variant: 'destructive', + title: "复制失败", + description: error instanceof Error ? error.message : "复制计划失败", + variant: "destructive", }); } }; @@ -192,24 +224,28 @@ export default function ScenarioDetail() { const response = await deletePlan(taskId); if (response && response.code === 200) { toast({ - title: '计划已删除', + title: "计划已删除", description: `已成功删除"${taskToDelete.name}"`, }); - + // 重新加载数据 const refreshResponse = await fetchPlanList(scenarioId!, 1, 20); - if (refreshResponse && refreshResponse.code === 200 && refreshResponse.data) { + if ( + refreshResponse && + refreshResponse.code === 200 && + refreshResponse.data + ) { setTasks(refreshResponse.data.list); } } else { - throw new Error(response?.msg || '删除失败'); + throw new Error(response?.msg || "删除失败"); } } catch (error) { - console.error('删除计划失败:', error); + console.error("删除计划失败:", error); toast({ - title: '删除失败', - description: error instanceof Error ? error.message : '删除计划失败', - variant: 'destructive', + title: "删除失败", + description: error instanceof Error ? error.message : "删除计划失败", + variant: "destructive", }); } }; @@ -217,20 +253,22 @@ export default function ScenarioDetail() { const handleStatusChange = async (taskId: string, newStatus: 1 | 0) => { try { // 这里应该调用状态切换API,暂时模拟 - setTasks(prev => prev.map(task => - task.id === taskId ? { ...task, status: newStatus } : task - )); - + setTasks((prev) => + prev.map((task) => + task.id === taskId ? { ...task, status: newStatus } : task + ) + ); + toast({ - title: '状态已更新', - description: `计划已${newStatus === 1 ? '启动' : '暂停'}`, + title: "状态已更新", + description: `计划已${newStatus === 1 ? "启动" : "暂停"}`, }); } catch (error) { - console.error('状态切换失败:', error); + console.error("状态切换失败:", error); toast({ - title: '状态切换失败', - description: '请稍后重试', - variant: 'destructive', + title: "状态切换失败", + description: "请稍后重试", + variant: "destructive", }); } }; @@ -240,20 +278,22 @@ export default function ScenarioDetail() { const response = await fetchPlanDetail(taskId); if (response && response.code === 200 && response.data) { setCurrentApiSettings({ - apiKey: response.data.apiKey || 'demo-api-key-123456', - webhookUrl: response.data.textUrl?.fullUrl || `https://api.example.com/webhook/${taskId}`, + apiKey: response.data.apiKey || "demo-api-key-123456", + webhookUrl: + response.data.textUrl?.fullUrl || + `https://api.example.com/webhook/${taskId}`, taskId, }); setShowApiDialog(true); } else { - throw new Error(response?.msg || '获取API设置失败'); + throw new Error(response?.msg || "获取API设置失败"); } } catch (error) { - console.error('获取API设置失败:', error); + console.error("获取API设置失败:", error); toast({ - title: '获取API设置失败', - description: '请稍后重试', - variant: 'destructive', + title: "获取API设置失败", + description: "请稍后重试", + variant: "destructive", }); } }; @@ -261,34 +301,34 @@ export default function ScenarioDetail() { const handleCopyApiUrl = (url: string) => { navigator.clipboard.writeText(url); toast({ - title: '已复制', - description: '接口地址已复制到剪贴板', + title: "已复制", + description: "接口地址已复制到剪贴板", }); }; const handleCreateNewPlan = () => { - navigate(`/scenarios/new?scenario=${scenarioId}`); + navigate(`/scenarios/new/${scenarioId}`); }; const getStatusColor = (status: number) => { switch (status) { case 1: - return 'text-green-600 bg-green-50'; + return "text-green-600 bg-green-50"; case 0: - return 'text-yellow-600 bg-yellow-50'; + return "text-yellow-600 bg-yellow-50"; default: - return 'text-gray-600 bg-gray-50'; + return "text-gray-600 bg-gray-50"; } }; const getStatusText = (status: number) => { switch (status) { case 1: - return '进行中'; + return "进行中"; case 0: - return '已暂停'; + return "已暂停"; default: - return '未知'; + return "未知"; } }; @@ -297,11 +337,10 @@ export default function ScenarioDetail() { } - footer={} >
@@ -316,25 +355,20 @@ export default function ScenarioDetail() { if (error) { return ( - } + header={} footer={} >

{error}

- +
-
); } @@ -342,12 +376,7 @@ export default function ScenarioDetail() { if (!scenario) { return ( - } + header={} footer={} >
@@ -355,141 +384,147 @@ export default function ScenarioDetail() {

加载场景数据中...

-
+
); } + const handleRefresh = async () => { + setLoadingTasks(true); + await fetchPlanList(scenarioId!, 1, 20); + setLoadingTasks(false); + }; + + const filteredTasks = tasks.filter((task) => task.name.includes(searchTerm)); + return ( - - 新建计划 - - } - /> - } - footer={} - > -
-
- {/* 场景描述 */} -
-

{scenario.description}

-
- - {/* 数据统计 */} - {/*
-
-
-
-

总获客数

-

{scenario.totalCustomers}

-
- -
-
- - {scenario.growth} -
-
- -
-
-
-

今日获客

-

{scenario.todayCustomers}

-
- -
-
- 活跃计划: {scenario.totalPlans} -
-
-
*/} - - {/* 计划列表 */} -
-
-

获客计划

-
- - {tasks.length === 200 ? ( -
-
- -

暂无获客计划

-

创建您的第一个获客计划,开始获取客户

-
+ <> + - - 创建第一个计划 + + 新建计划 + + } + /> +
+
+ + setSearchTerm(e.target.value)} + /> +
+ +
+ + } + > +
+ {/* 计划列表 */} +
+ {filteredTasks.length === 0 ? ( +
+
+ +

+ 暂无获客计划 +

+

+ 创建您的第一个获客计划,开始获取客户 +

+
+
) : (
- {tasks.map((task) => ( -
+ {filteredTasks.map((task) => ( +
-

{task.name}

- - {getStatusText(task.status)} +

+ {task.name} +

+ + {getStatusText(task.status)}
- 最后更新: {task.lastUpdated} + 最后更新: {task.lastUpdated} +
+
+ + 设备: {task.stats?.devices || 0} | 获客:{" "} + {task.stats?.acquired || 0} | 添加:{" "} + {task.stats?.added || 0} +
-
- 设备: {task.stats?.devices || 0} | 获客: {task.stats?.acquired || 0} | 添加: {task.stats?.added || 0}
-
- -
- - - - - - - + +
+ + + + + + +
@@ -498,8 +533,6 @@ export default function ScenarioDetail() { )}
-
- {/* API接口设置对话框 */} {showApiDialog && (
@@ -511,7 +544,9 @@ export default function ScenarioDetail() {

计划接口配置

-

通过API接口直接导入客资到该获客计划

+

+ 通过API接口直接导入客资到该获客计划 +

@@ -560,7 +598,9 @@ export default function ScenarioDetail() {

接口地址

- POST请求 + + POST请求 +
- +
-
必要参数
+
+ 必要参数 +
-
name - 客户姓名
-
phone - 手机号码
+
+ name{" "} + - 客户姓名 +
+
+ phone{" "} + - 手机号码 +
可选参数
-
source - 来源标识
-
remark - 备注信息
-
tags - 客户标签
+
+ source{" "} + - 来源标识 +
+
+ remark{" "} + - 备注信息 +
+
+ tags - + 客户标签 +
@@ -601,4 +660,4 @@ export default function ScenarioDetail() { )} ); -} \ No newline at end of file +} diff --git a/nkebao/src/pages/scenarios/Scenarios.tsx b/nkebao/src/pages/scenarios/Scenarios.tsx index 721fd198..6fe27e81 100644 --- a/nkebao/src/pages/scenarios/Scenarios.tsx +++ b/nkebao/src/pages/scenarios/Scenarios.tsx @@ -1,11 +1,11 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Plus, TrendingUp, Loader2 } from 'lucide-react'; -import UnifiedHeader from '@/components/UnifiedHeader'; -import Layout from '@/components/Layout'; -import BottomNav from '@/components/BottomNav'; -import { fetchScenes, type SceneItem } from '@/api/scenarios'; -import '@/components/Layout.css'; +import React, { useEffect, useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { Plus, TrendingUp, Loader2 } from "lucide-react"; +import UnifiedHeader from "@/components/UnifiedHeader"; +import Layout from "@/components/Layout"; +import BottomNav from "@/components/BottomNav"; +import { fetchScenes, type SceneItem } from "@/api/scenarios"; +import "@/components/Layout.css"; interface Scenario { id: string; @@ -21,45 +21,54 @@ export default function Scenarios() { const navigate = useNavigate(); const [scenarios, setScenarios] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); + const [error, setError] = useState(""); // 场景描述映射 - const scenarioDescriptions: Record = useMemo(() => ({ - douyin: '通过抖音平台进行精准获客', - xiaohongshu: '利用小红书平台进行内容营销获客', - gongzhonghao: '通过微信公众号进行获客', - haibao: '通过海报分享进行获客', - phone: '通过电话营销进行获客', - weixinqun: '通过微信群进行获客', - payment: '通过付款码进行获客', - api: '通过API接口进行获客', - }), []); + const scenarioDescriptions: Record = useMemo( + () => ({ + douyin: "通过抖音平台进行精准获客", + xiaohongshu: "利用小红书平台进行内容营销获客", + gongzhonghao: "通过微信公众号进行获客", + haibao: "通过海报分享进行获客", + phone: "通过电话营销进行获客", + weixinqun: "通过微信群进行获客", + payment: "通过付款码进行获客", + api: "通过API接口进行获客", + }), + [] + ); useEffect(() => { const fetchScenarios = async () => { setLoading(true); - setError(''); - + setError(""); + try { const response = await fetchScenes({ page: 1, limit: 20 }); - + if (response && response.code === 200 && response.data) { // 转换API数据为前端需要的格式 - const transformedScenarios: Scenario[] = response.data.map((item: SceneItem) => ({ - id: item.id.toString(), - name: item.name, - image: item.image|| 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png', - description: scenarioDescriptions[item.name.toLowerCase()] || '通过该平台进行获客', - count: Math.floor(Math.random() * 200) + 50, // 模拟今日数据 - growth: `+${Math.floor(Math.random() * 20) + 5}%`, // 模拟增长率 - status: item.status === 1 ? 'active' : 'inactive', - })); - + const transformedScenarios: Scenario[] = response.data.map( + (item: SceneItem) => ({ + id: item.id.toString(), + name: item.name, + image: + item.image || + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png", + description: + scenarioDescriptions[item.name.toLowerCase()] || + "通过该平台进行获客", + count: Math.floor(Math.random() * 200) + 50, // 模拟今日数据 + growth: `+${Math.floor(Math.random() * 20) + 5}%`, // 模拟增长率 + status: item.status === 1 ? "active" : "inactive", + }) + ); + setScenarios(transformedScenarios); - } + } } catch (error) { - console.error('获取场景数据失败:', error); - setError('获取场景数据失败,请稍后重试'); + console.error("获取场景数据失败:", error); + setError("获取场景数据失败,请稍后重试"); } finally { setLoading(false); } @@ -68,23 +77,20 @@ export default function Scenarios() { fetchScenarios(); }, [scenarioDescriptions]); - const handleScenarioClick = (scenarioId: string,scenarioName:string) => { - navigate(`/scenarios/${scenarioId}?name=${encodeURIComponent(scenarioName)}`); + const handleScenarioClick = (scenarioId: string, scenarioName: string) => { + navigate( + `/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}` + ); }; const handleNewPlan = () => { - navigate('/scenarios/new'); + navigate("/scenarios/new"); }; if (loading) { return ( - } + header={} footer={} >
@@ -100,18 +106,13 @@ export default function Scenarios() { if (error && scenarios.length === 0) { return ( - } + header={} footer={} >

{error}

-
)} - +
{scenarios.map((scenario) => (
handleScenarioClick(scenario.id,scenario.name)} + onClick={() => handleScenarioClick(scenario.id, scenario.name)} >
- {scenario.name} { // 图片加载失败时使用默认图标 - e.currentTarget.src = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png'; + e.currentTarget.src = + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png"; }} />
-

{scenario.name}

+

+ {scenario.name} +

{scenario.description && ( -

{scenario.description}

+

+ {scenario.description} +

)}
今日: @@ -190,4 +196,4 @@ export default function Scenarios() {
); -} \ No newline at end of file +} diff --git a/nkebao/src/pages/scenarios/new/page.tsx b/nkebao/src/pages/scenarios/new/page.tsx index 835557fa..0bc6bb47 100644 --- a/nkebao/src/pages/scenarios/new/page.tsx +++ b/nkebao/src/pages/scenarios/new/page.tsx @@ -1,135 +1,246 @@ -import { useState, useEffect } from "react" -import { useNavigate } from "react-router-dom" -import { ChevronLeft, Settings } from "lucide-react" -import { Button } from "@/components/ui/button" -import { toast } from "@/components/ui/use-toast" -import { Steps, StepItem } from 'tdesign-mobile-react'; -import { BasicSettings } from "./steps/BasicSettings" -import { FriendRequestSettings } from "./steps/FriendRequestSettings" -import { MessageSettings } from "./steps/MessageSettings" -import Layout from "@/components/Layout" -import { getPlanScenes } from '@/api/scenarios'; +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { ChevronLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Toast } from "tdesign-mobile-react"; +import { Steps, StepItem } from "tdesign-mobile-react"; +import { BasicSettings } from "./steps/BasicSettings"; +import { FriendRequestSettings } from "./steps/FriendRequestSettings"; +import { MessageSettings } from "./steps/MessageSettings"; +import Layout from "@/components/Layout"; +import { + getPlanScenes, + createScenarioPlan, + fetchPlanDetail, + PlanDetail, + updateScenarioPlan, +} from "@/api/scenarios"; // 步骤定义 - 只保留三个步骤 const steps = [ { id: 1, title: "步骤一", subtitle: "基础设置" }, { id: 2, title: "步骤二", subtitle: "好友申请设置" }, { id: 3, title: "步骤三", subtitle: "消息设置" }, -] +]; + +// 类型定义 +interface FormData { + name: string; + scenario: number; + posters: any[]; // 后续可替换为具体Poster类型 + device: string[]; + remarkType: string; + greeting: string; + addInterval: number; + startTime: string; + endTime: string; + enabled: boolean; + sceneId: string | number; + remarkFormat: string; + addFriendInterval: number; +} export default function NewPlan() { - const router = useNavigate() - const [currentStep, setCurrentStep] = useState(1) - const [formData, setFormData] = useState({ - planName: "", - scenario: "haibao", + const router = useNavigate(); + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState({ + name: "", + scenario: 1, posters: [], - device: "", + device: [], remarkType: "phone", greeting: "你好,请通过", addInterval: 1, startTime: "09:00", endTime: "18:00", enabled: true, - // 移除tags字段 - }) + sceneId: "", + remarkFormat: "", + addFriendInterval: 1, + }); const [sceneList, setSceneList] = useState([]); const [sceneLoading, setSceneLoading] = useState(true); - + const { scenarioId, planId } = useParams<{ + scenarioId: string; + planId: string; + }>(); + const [isEdit, setIsEdit] = useState(false); useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { setSceneLoading(true); + //获取场景类型 getPlanScenes() - .then(res => { + .then((res) => { setSceneList(res?.data || []); }) .finally(() => setSceneLoading(false)); - }, []); + if (planId) { + setIsEdit(true); + //获取计划详情 + const res = await fetchPlanDetail(planId); + if (res.code === 200 && res.data) { + const detail = res.data as PlanDetail; + 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) { + setFormData((prev) => ({ + ...prev, + ...{ scenario: Number(scenarioId) || 1 }, + })); + } + } + }; // 更新表单数据 const onChange = (data: any) => { - setFormData((prev) => ({ ...prev, ...data })) - } + setFormData((prev) => ({ ...prev, ...data })); + }; // 处理保存 const handleSave = async () => { try { - // 这里应该是实际的API调用 - await new Promise((resolve) => setTimeout(resolve, 1000)) - - toast({ - title: "创建成功", - description: "获客计划已创建", - }) - router("/plans") + let result; + if (isEdit && planId) { + // 编辑:拼接后端需要的完整参数 + const editData = { + ...formData, + id: Number(planId), + planId: Number(planId), + // 兼容后端需要的字段 + // 你可以根据实际需要补充其它字段 + }; + result = await updateScenarioPlan(planId, editData); + } else { + // 新建 + result = await createScenarioPlan(formData); + } + if (result.code === 200) { + Toast({ + message: isEdit ? "计划已更新" : "获客计划已创建", + theme: "success", + }); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + router(`/scenarios/list/${formData.sceneId}/${sceneItem.name}`); + } else { + Toast({ message: result.msg, theme: "error" }); + } } catch (error) { - toast({ - title: "创建失败", - description: "创建计划失败,请重试", - variant: "destructive", - }) + Toast({ + message: + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : isEdit + ? "更新计划失败,请重试" + : "创建计划失败,请重试", + theme: "error", + }); } - } + }; // 下一步 const handleNext = () => { if (currentStep === steps.length) { - handleSave() + handleSave(); } else { - setCurrentStep((prev) => prev + 1) + setCurrentStep((prev) => prev + 1); } - } + }; // 上一步 const handlePrev = () => { - setCurrentStep((prev) => Math.max(prev - 1, 1)) - } + setCurrentStep((prev) => Math.max(prev - 1, 1)); + }; // 渲染当前步骤内容 const renderStepContent = () => { switch (currentStep) { case 1: - return + return ( + + ); case 2: - return + return ( + + ); case 3: - return + return ( + + ); default: - return null + return null; } - } + }; return ( + +
+
+
+ +
+
+
- -
-
-
- - +
+ + {steps.map((step) => ( + + ))} +
- -
-
- -
- - {steps.map((step) => ( - - ))} - -
- - }> - -
- {renderStepContent()} -
+ + } + > +
{renderStepContent()}
- - ) + ); } diff --git a/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx index b62d7064..7b215865 100644 --- a/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx @@ -5,167 +5,11 @@ import { Input, Tag, Grid, - Dialog, ImageViewer, - Table, Switch, } from "tdesign-mobile-react"; import EyeIcon from "@/components/icons/EyeIcon"; -import UploadImage from "@/components/UploadImage"; - -const phoneCallTags = [ - { id: "tag-1", name: "咨询", color: "bg-blue-100 text-blue-800" }, - { id: "tag-2", name: "投诉", color: "bg-red-100 text-red-800" }, - { id: "tag-3", name: "合作", color: "bg-green-100 text-green-800" }, - { id: "tag-4", name: "价格", color: "bg-orange-100 text-orange-800" }, - { id: "tag-5", name: "售后", color: "bg-purple-100 text-purple-800" }, - { id: "tag-6", name: "订单", color: "bg-yellow-100 text-yellow-800" }, - { id: "tag-7", name: "物流", color: "bg-teal-100 text-teal-800" }, -]; - -// 不同场景的预设标签 -const scenarioTags = { - haibao: [ - { - id: "poster-tag-1", - name: "活动推广", - color: "bg-blue-100 text-blue-800", - }, - { - id: "poster-tag-2", - name: "产品宣传", - color: "bg-green-100 text-green-800", - }, - { - id: "poster-tag-3", - name: "品牌展示", - color: "bg-purple-100 text-purple-800", - }, - { id: "poster-tag-4", name: "优惠促销", color: "bg-red-100 text-red-800" }, - { - id: "poster-tag-5", - name: "新品发布", - color: "bg-orange-100 text-orange-800", - }, - ], - order: [ - { id: "order-tag-1", name: "新订单", color: "bg-green-100 text-green-800" }, - { id: "order-tag-2", name: "复购客户", color: "bg-blue-100 text-blue-800" }, - { - id: "order-tag-3", - name: "高价值订单", - color: "bg-purple-100 text-purple-800", - }, - { - id: "order-tag-4", - name: "待付款", - color: "bg-yellow-100 text-yellow-800", - }, - { id: "order-tag-5", name: "已完成", color: "bg-gray-100 text-gray-800" }, - ], - douyin: [ - { id: "douyin-tag-1", name: "短视频", color: "bg-pink-100 text-pink-800" }, - { id: "douyin-tag-2", name: "直播", color: "bg-red-100 text-red-800" }, - { - id: "douyin-tag-3", - name: "带货", - color: "bg-orange-100 text-orange-800", - }, - { - id: "douyin-tag-4", - name: "粉丝互动", - color: "bg-blue-100 text-blue-800", - }, - { - id: "douyin-tag-5", - name: "热门话题", - color: "bg-purple-100 text-purple-800", - }, - ], - xiaohongshu: [ - { id: "xhs-tag-1", name: "种草笔记", color: "bg-red-100 text-red-800" }, - { id: "xhs-tag-2", name: "美妆", color: "bg-pink-100 text-pink-800" }, - { id: "xhs-tag-3", name: "穿搭", color: "bg-purple-100 text-purple-800" }, - { id: "xhs-tag-4", name: "生活方式", color: "bg-green-100 text-green-800" }, - { - id: "xhs-tag-5", - name: "好物推荐", - color: "bg-orange-100 text-orange-800", - }, - ], - phone: phoneCallTags, - gongzhonghao: [ - { id: "gzh-tag-1", name: "文章推送", color: "bg-blue-100 text-blue-800" }, - { id: "gzh-tag-2", name: "活动通知", color: "bg-green-100 text-green-800" }, - { - id: "gzh-tag-3", - name: "产品介绍", - color: "bg-purple-100 text-purple-800", - }, - { - id: "gzh-tag-4", - name: "用户服务", - color: "bg-orange-100 text-orange-800", - }, - { id: "gzh-tag-5", name: "品牌故事", color: "bg-gray-100 text-gray-800" }, - ], - weixinqun: [ - { id: "wxq-tag-1", name: "群活动", color: "bg-green-100 text-green-800" }, - { id: "wxq-tag-2", name: "产品分享", color: "bg-blue-100 text-blue-800" }, - { - id: "wxq-tag-3", - name: "用户交流", - color: "bg-purple-100 text-purple-800", - }, - { id: "wxq-tag-4", name: "优惠信息", color: "bg-pink-100 text-pink-800" }, - { - id: "wxq-tag-5", - name: "答疑解惑", - color: "bg-orange-100 text-orange-800", - }, - { - id: "wxq-tag-6", - name: "新人欢迎", - color: "bg-yellow-100 text-yellow-800", - }, - { id: "wxq-tag-7", name: "群规通知", color: "bg-gray-100 text-gray-800" }, - { - id: "wxq-tag-8", - name: "活跃互动", - color: "bg-indigo-100 text-indigo-800", - }, - ], - payment: [ - { id: "pay-tag-1", name: "扫码支付", color: "bg-green-100 text-green-800" }, - { id: "pay-tag-2", name: "线下门店", color: "bg-blue-100 text-blue-800" }, - { - id: "pay-tag-3", - name: "活动收款", - color: "bg-purple-100 text-purple-800", - }, - { - id: "pay-tag-4", - name: "服务费用", - color: "bg-orange-100 text-orange-800", - }, - { - id: "pay-tag-5", - name: "会员充值", - color: "bg-yellow-100 text-yellow-800", - }, - ], - api: [ - { id: "api-tag-1", name: "系统对接", color: "bg-blue-100 text-blue-800" }, - { id: "api-tag-2", name: "数据同步", color: "bg-green-100 text-green-800" }, - { id: "api-tag-3", name: "自动化", color: "bg-purple-100 text-purple-800" }, - { - id: "api-tag-4", - name: "第三方平台", - color: "bg-orange-100 text-orange-800", - }, - { id: "api-tag-5", name: "实时推送", color: "bg-gray-100 text-gray-800" }, - ], -}; +import { uploadFile } from "@/api/utils"; interface BasicSettingsProps { formData: any; @@ -334,12 +178,18 @@ export function BasicSettings({ onChange({ ...formData, scenario: "haibao" }); } - if (!formData.planName) { - const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); - onChange({ ...formData, planName: `海报${today}` }); + // 检查是否已经有上传的订单文件 + 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}` }); + }, [sceneList]); + // 选中场景 const handleScenarioSelect = (sceneId: number) => { onChange({ ...formData, scenario: sceneId }); @@ -500,6 +350,28 @@ export function BasicSettings({ 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); @@ -559,9 +431,9 @@ export function BasicSettings({ - onChange({ ...formData, planName: String(e.target.value) }) + onChange({ ...formData, name: String(e.target.value) }) } placeholder="请输入计划名称" /> @@ -816,41 +688,35 @@ export function BasicSettings({
支持 CSV、Excel 格式,上传后将文件保存到服务器
- {/* 已导入数据表格可复用原有Table渲染 */} - {importedTags.length > 0 && ( - - )} {/* 电话获客设置区块,仅在选择电话获客场景时显示 */} {formData.scenario === 5 && ( @@ -940,6 +806,21 @@ export function BasicSettings({ )} + +
+ 是否启用 + onChange({ ...formData, enabled: value })} + /> +
diff --git a/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx index cbfa37cf..b49fc030 100644 --- a/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx +++ b/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx @@ -1,23 +1,28 @@ -"use client" +"use client"; -import { useState, useEffect } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { HelpCircle, MessageSquare, AlertCircle } from "lucide-react" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { ChevronsUpDown } from "lucide-react" -import { Checkbox } from "@/components/ui/checkbox" +import { useState, useEffect } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +import { HelpCircle, MessageSquare, AlertCircle } from "lucide-react"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { ChevronsUpDown } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import DeviceSelection from "@/components/DeviceSelection"; interface FriendRequestSettingsProps { - formData: any - onChange: (data: any) => void - onNext: () => void - onPrev: () => void + formData: any; + onChange: (data: any) => void; + onNext: () => void; + onPrev: () => void; } // 招呼语模板 @@ -27,45 +32,43 @@ const greetingTemplates = [ "你好,我是XX产品的客服请通过", "你好,感谢关注我们的产品", "你好,很高兴为您服务", -] +]; // 备注类型选项 const remarkTypes = [ { value: "phone", label: "手机号" }, { value: "nickname", label: "昵称" }, { value: "source", label: "来源" }, -] +]; -// 模拟设备数据 -const mockDevices = [ - { id: "1", name: "iPhone 13 Pro", status: "online" }, - { id: "2", name: "Xiaomi 12", status: "online" }, - { id: "3", name: "Huawei P40", status: "offline" }, - { id: "4", name: "OPPO Find X3", status: "online" }, - { id: "5", name: "Samsung S21", status: "online" }, -] - -export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: FriendRequestSettingsProps) { - const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false) - const [hasWarnings, setHasWarnings] = useState(false) - const [isDeviceSelectorOpen, setIsDeviceSelectorOpen] = useState(false) - const [selectedDevices, setSelectedDevices] = useState(formData.selectedDevices || []) +export function FriendRequestSettings({ + formData, + onChange, + onNext, + onPrev, +}: FriendRequestSettingsProps) { + const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); + const [hasWarnings, setHasWarnings] = useState(false); + const [selectedDevices, setSelectedDevices] = useState( + formData.selectedDevices || [] + ); + const [showRemarkTip, setShowRemarkTip] = useState(false); // 获取场景标题 const getScenarioTitle = () => { switch (formData.scenario) { case "douyin": - return "抖音直播" + return "抖音直播"; case "xiaohongshu": - return "小红书" + return "小红书"; case "weixinqun": - return "微信群" + return "微信群"; case "gongzhonghao": - return "公众号" + return "公众号"; default: - return formData.planName || "获客计划" + return formData.name || "获客计划"; } - } + }; // 使用useEffect设置默认值 useEffect(() => { @@ -76,172 +79,158 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr remarkType: "phone", // 默认选择手机号 remarkFormat: `手机号+${getScenarioTitle()}`, // 默认备注格式 addFriendInterval: 1, - }) + }); } - }, [formData, formData.greeting, onChange]) + }, [formData, formData.greeting, onChange]); // 检查是否有未完成的必填项 useEffect(() => { - const hasIncompleteFields = !formData.greeting?.trim() - setHasWarnings(hasIncompleteFields) - }, [formData]) + const hasIncompleteFields = !formData.greeting?.trim(); + setHasWarnings(hasIncompleteFields); + }, [formData]); const handleTemplateSelect = (template: string) => { - onChange({ ...formData, greeting: template }) - setIsTemplateDialogOpen(false) - } + onChange({ ...formData, greeting: template }); + setIsTemplateDialogOpen(false); + }; const handleNext = () => { // 即使有警告也允许进入下一步,但会显示提示 - onNext() - } - - const toggleDeviceSelection = (device: any) => { - const isSelected = selectedDevices.some((d) => d.id === device.id) - let newSelectedDevices - - if (isSelected) { - newSelectedDevices = selectedDevices.filter((d) => d.id !== device.id) - } else { - newSelectedDevices = [...selectedDevices, device] - } - - setSelectedDevices(newSelectedDevices) - onChange({ ...formData, selectedDevices: newSelectedDevices }) - } + onNext(); + }; return ( - + <>
- -
- + 选择设备 +
+ d.id)} + onSelect={(deviceIds) => { + const newSelectedDevices = deviceIds.map((id) => ({ + id, + name: `设备 ${id}`, + status: "online", + })); + setSelectedDevices(newSelectedDevices); + onChange({ ...formData, device: deviceIds }); + }} + placeholder="选择设备" + /> +
+
- {isDeviceSelectorOpen && ( -
-
- -
- {mockDevices.map((device) => ( -
toggleDeviceSelection(device)} - > -
- d.id === device.id)} - onCheckedChange={() => toggleDeviceSelection(device)} - /> - {device.name} -
- - {device.status === "online" ? "在线" : "离线"} - -
- ))} -
+
+
+ 好友备注 + setShowRemarkTip(true)} + onMouseLeave={() => setShowRemarkTip(false)} + onClick={() => setShowRemarkTip((v) => !v)} + > + ? + + {showRemarkTip && ( +
+
设置添加好友时的备注格式
+
备注格式预览:
+
+ {formData.remarkType === "phone" && + `138****1234+${getScenarioTitle()}`} + {formData.remarkType === "nickname" && + `小红书用户2851+${getScenarioTitle()}`} + {formData.remarkType === "source" && + `抖音直播+${getScenarioTitle()}`}
)}
-
- -
-
- - - - - - - -

设置添加好友时的备注格式

-

备注格式预览:

-

{formData.remarkType === "phone" && `138****1234+${getScenarioTitle()}`}

-

{formData.remarkType === "nickname" && `小红书用户2851+${getScenarioTitle()}`}

-

{formData.remarkType === "source" && `抖音直播+${getScenarioTitle()}`}

-
-
-
-
- + {remarkTypes.map((type) => ( + + ))} +
- -
onChange({ ...formData, greeting: e.target.value })} + onChange={(e) => + onChange({ ...formData, greeting: e.target.value }) + } placeholder="请输入招呼语" className="mt-2" />
- + 添加间隔
onChange({ ...formData, addFriendInterval: Number(e.target.value) })} - className="w-32" + onChange={(e) => + onChange({ + ...formData, + addFriendInterval: Number(e.target.value), + }) + } /> - 分钟 +
分钟
- + 允许加人的时间段
onChange({ ...formData, addFriendTimeStart: e.target.value })} + onChange={(e) => + onChange({ ...formData, addFriendTimeStart: e.target.value }) + } className="w-32" /> onChange({ ...formData, addFriendTimeEnd: e.target.value })} + onChange={(e) => + onChange({ ...formData, addFriendTimeEnd: e.target.value }) + } className="w-32" />
{hasWarnings && ( - + - 您有未完成的设置项,建议完善后再进入下一步。 + + 您有未完成的设置项,建议完善后再进入下一步。 + )} @@ -253,8 +242,11 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
- - + + 招呼语模板 @@ -272,6 +264,6 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
- - ) + + ); } diff --git a/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx b/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx index 3c8fc4a9..4dbf96fc 100644 --- a/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx +++ b/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx @@ -1,9 +1,7 @@ -import { useState } from "react" -import { Card } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { MessageSquare, ImageIcon, @@ -16,40 +14,46 @@ import { X, Upload, Clock, -} from "lucide-react" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { toast } from "@/components/ui/use-toast" +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { toast } from "@/components/ui/use-toast"; interface MessageContent { - id: string - type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group" - content: string - sendInterval?: number - intervalUnit?: "seconds" | "minutes" + id: string; + type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group"; + content: string; + sendInterval?: number; + intervalUnit?: "seconds" | "minutes"; scheduledTime?: { - hour: number - minute: number - second: number - } - title?: string - description?: string - address?: string - coverImage?: string - groupId?: string - linkUrl?: string + hour: number; + minute: number; + second: number; + }; + title?: string; + description?: string; + address?: string; + coverImage?: string; + groupId?: string; + linkUrl?: string; } interface DayPlan { - day: number - messages: MessageContent[] + day: number; + messages: MessageContent[]; } interface MessageSettingsProps { - formData: any - onChange: (data: any) => void - onNext: () => void - onPrev: () => void + formData: any; + onChange: (data: any) => void; + onNext: () => void; + onPrev: () => void; } // 消息类型配置 @@ -61,16 +65,21 @@ const messageTypes = [ { id: "miniprogram", icon: Window, label: "小程序" }, { id: "link", icon: Link2, label: "链接" }, { id: "group", icon: Users, label: "邀请入群" }, -] +]; // 模拟群组数据 const mockGroups = [ { id: "1", name: "产品交流群1", memberCount: 156 }, { id: "2", name: "产品交流群2", memberCount: 234 }, { id: "3", name: "产品交流群3", memberCount: 89 }, -] +]; -export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageSettingsProps) { +export function MessageSettings({ + formData, + onChange, + onNext, + onPrev, +}: MessageSettingsProps) { const [dayPlans, setDayPlans] = useState([ { day: 0, @@ -84,67 +93,71 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS }, ], }, - ]) - const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false) - const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false) - const [selectedGroupId, setSelectedGroupId] = useState("") + ]); + const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false); + const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false); + const [selectedGroupId, setSelectedGroupId] = useState(""); // 添加新消息 const handleAddMessage = (dayIndex: number, type = "text") => { - const updatedPlans = [...dayPlans] + const updatedPlans = [...dayPlans]; const newMessage: MessageContent = { id: Date.now().toString(), type: type as MessageContent["type"], content: "", - } + }; if (dayPlans[dayIndex].day === 0) { // 即时消息使用间隔设置 - newMessage.sendInterval = 5 - newMessage.intervalUnit = "seconds" // 默认改为秒 + newMessage.sendInterval = 5; + newMessage.intervalUnit = "seconds"; // 默认改为秒 } else { // 非即时消息使用具体时间设置 newMessage.scheduledTime = { hour: 9, minute: 0, second: 0, - } + }; } - updatedPlans[dayIndex].messages.push(newMessage) - setDayPlans(updatedPlans) - onChange({ ...formData, messagePlans: updatedPlans }) - } + updatedPlans[dayIndex].messages.push(newMessage); + setDayPlans(updatedPlans); + onChange({ ...formData, messagePlans: updatedPlans }); + }; // 更新消息内容 - const handleUpdateMessage = (dayIndex: number, messageIndex: number, updates: Partial) => { - const updatedPlans = [...dayPlans] + const handleUpdateMessage = ( + dayIndex: number, + messageIndex: number, + updates: Partial + ) => { + const updatedPlans = [...dayPlans]; updatedPlans[dayIndex].messages[messageIndex] = { ...updatedPlans[dayIndex].messages[messageIndex], ...updates, - } - setDayPlans(updatedPlans) - onChange({ ...formData, messagePlans: updatedPlans }) - } + }; + setDayPlans(updatedPlans); + onChange({ ...formData, messagePlans: updatedPlans }); + }; // 删除消息 const handleRemoveMessage = (dayIndex: number, messageIndex: number) => { - const updatedPlans = [...dayPlans] - updatedPlans[dayIndex].messages.splice(messageIndex, 1) - setDayPlans(updatedPlans) - onChange({ ...formData, messagePlans: updatedPlans }) - } + const updatedPlans = [...dayPlans]; + updatedPlans[dayIndex].messages.splice(messageIndex, 1); + setDayPlans(updatedPlans); + onChange({ ...formData, messagePlans: updatedPlans }); + }; // 切换时间单位 const toggleIntervalUnit = (dayIndex: number, messageIndex: number) => { - const message = dayPlans[dayIndex].messages[messageIndex] - const newUnit = message.intervalUnit === "minutes" ? "seconds" : "minutes" - handleUpdateMessage(dayIndex, messageIndex, { intervalUnit: newUnit }) - } + const message = dayPlans[dayIndex].messages[messageIndex]; + const newUnit = message.intervalUnit === "minutes" ? "seconds" : "minutes"; + handleUpdateMessage(dayIndex, messageIndex, { intervalUnit: newUnit }); + }; // 添加新的天数计划 const handleAddDayPlan = () => { - const newDay = dayPlans.length + const newDay = dayPlans.length; setDayPlans([ ...dayPlans, { @@ -162,47 +175,63 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS }, ], }, - ]) - setIsAddDayPlanOpen(false) + ]); + setIsAddDayPlanOpen(false); toast({ title: "添加成功", description: `已添加第${newDay}天的消息计划`, - }) - } + }); + }; // 选择群组 const handleSelectGroup = (groupId: string) => { - setSelectedGroupId(groupId) - setIsGroupSelectOpen(false) + setSelectedGroupId(groupId); + setIsGroupSelectOpen(false); toast({ title: "选择成功", - description: `已选择群组:${mockGroups.find((g) => g.id === groupId)?.name}`, - }) - } + description: `已选择群组:${ + mockGroups.find((g) => g.id === groupId)?.name + }`, + }); + }; // 处理文件上传 - const handleFileUpload = (dayIndex: number, messageIndex: number, type: "image" | "video" | "file") => { + const handleFileUpload = ( + dayIndex: number, + messageIndex: number, + type: "image" | "video" | "file" + ) => { // 模拟文件上传 toast({ title: "上传成功", - description: `${type === "image" ? "图片" : type === "video" ? "视频" : "文件"}上传成功`, - }) - } + description: `${ + type === "image" ? "图片" : type === "video" ? "视频" : "文件" + }上传成功`, + }); + }; return ( - + <>

消息设置

-
- + {dayPlans.map((plan) => ( - + {plan.day === 0 ? "即时消息" : `第${plan.day}天`} ))} @@ -212,33 +241,45 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS
{plan.messages.map((message, messageIndex) => ( -
+
{plan.day === 0 ? ( <> - - - handleUpdateMessage(dayIndex, messageIndex, { sendInterval: Number(e.target.value) }) - } - className="w-20" - /> +
间隔
+
+ + handleUpdateMessage(dayIndex, messageIndex, { + sendInterval: Number(e.target.value), + }) + } + /> +
) : ( <> - +
发送时间
handleUpdateMessage(dayIndex, messageIndex, { scheduledTime: { - ...(message.scheduledTime || { hour: 0, minute: 0, second: 0 }), + ...(message.scheduledTime || { + hour: 0, + minute: 0, + second: 0, + }), hour: Number(e.target.value), }, }) @@ -260,11 +305,17 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS type="number" min={0} max={59} - value={String(message.scheduledTime?.minute || 0)} + value={String( + message.scheduledTime?.minute || 0 + )} onChange={(e) => handleUpdateMessage(dayIndex, messageIndex, { scheduledTime: { - ...(message.scheduledTime || { hour: 0, minute: 0, second: 0 }), + ...(message.scheduledTime || { + hour: 0, + minute: 0, + second: 0, + }), minute: Number(e.target.value), }, }) @@ -276,11 +327,17 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS type="number" min={0} max={59} - value={String(message.scheduledTime?.second || 0)} + value={String( + message.scheduledTime?.second || 0 + )} onChange={(e) => handleUpdateMessage(dayIndex, messageIndex, { scheduledTime: { - ...(message.scheduledTime || { hour: 0, minute: 0, second: 0 }), + ...(message.scheduledTime || { + hour: 0, + minute: 0, + second: 0, + }), second: Number(e.target.value), }, }) @@ -291,7 +348,13 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS )}
-
@@ -300,9 +363,15 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS {messageTypes.map((type) => (