diff --git a/Cunkebao/api/wechat-accounts.ts b/Cunkebao/api/wechat-accounts.ts index 8190dd34..0f761498 100755 --- a/Cunkebao/api/wechat-accounts.ts +++ b/Cunkebao/api/wechat-accounts.ts @@ -65,9 +65,34 @@ export const transformWechatAccount = (serverAccount: any): import("@/types/wech let deviceName = ''; if (serverAccount.deviceInfo) { + // 尝试解析设备信息字符串 const deviceInfo = serverAccount.deviceInfo.split(' '); - deviceId = deviceInfo[0] || ''; - deviceName = deviceInfo[1] ? deviceInfo[1].replace(/[()]/g, '') : ''; + if (deviceInfo.length > 0) { + // 提取数字部分作为设备ID,确保是整数 + const possibleId = deviceInfo[0].trim(); + // 验证是否为数字 + deviceId = /^\d+$/.test(possibleId) ? possibleId : ''; + + // 提取设备名称 + if (deviceInfo.length > 1) { + deviceName = deviceInfo[1] ? deviceInfo[1].replace(/[()]/g, '').trim() : ''; + } + } + } + + // 如果从deviceInfo无法获取有效的设备ID,使用imei作为备选 + if (!deviceId && serverAccount.imei) { + deviceId = serverAccount.imei; + } + + // 如果仍然没有设备ID,使用微信账号的ID作为最后的备选 + if (!deviceId && serverAccount.id) { + deviceId = serverAccount.id.toString(); + } + + // 如果没有设备名称,使用备用名称 + if (!deviceName) { + deviceName = serverAccount.deviceMemo || '未命名设备'; } // 假设每天最多可添加20个好友 @@ -102,14 +127,34 @@ export const transformWechatAccountDetail = (detailResponse: WechatAccountDetail const { basicInfo, statistics, accountInfo, restrictions, friends } = detailResponse.data; - // 设备信息处理 + // 设备信息处理 - 改进处理方式 let deviceId = ''; let deviceName = ''; if (basicInfo.deviceInfo) { + // 尝试解析设备信息字符串 const deviceInfoParts = basicInfo.deviceInfo.split(' '); - deviceId = deviceInfoParts[0] || ''; - deviceName = deviceInfoParts[1] ? deviceInfoParts[1].replace(/[()]/g, '') : ''; + if (deviceInfoParts.length > 0) { + // 提取数字部分作为设备ID,确保是整数 + const possibleId = deviceInfoParts[0].trim(); + // 验证是否为数字 + deviceId = /^\d+$/.test(possibleId) ? possibleId : ''; + + // 提取设备名称 + if (deviceInfoParts.length > 1) { + deviceName = deviceInfoParts[1].replace(/[()]/g, '').trim(); + } + } + } + + // 如果从deviceInfo无法获取有效的设备ID,直接使用微信账号ID作为备选 + if (!deviceId && basicInfo.id) { + deviceId = basicInfo.id.toString(); + } + + // 如果没有设备名称,使用备用名称 + if (!deviceName) { + deviceName = '未命名设备'; } // 账号年龄计算 diff --git a/Cunkebao/app/contact-import/page.tsx b/Cunkebao/app/contact-import/page.tsx new file mode 100644 index 00000000..335dfed2 --- /dev/null +++ b/Cunkebao/app/contact-import/page.tsx @@ -0,0 +1,331 @@ +"use client"; + +import React, { useState } from "react"; +import * as XLSX from "xlsx"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; + +interface ContactData { + mobile: number; + from: string; + alias: string; +} + +export default function ContactImportPage() { + const [parsedData, setParsedData] = useState([]); + const [error, setError] = useState(null); + const [fileName, setFileName] = useState(""); + const [isImportSuccessful, setIsImportSuccessful] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [detectedColumns, setDetectedColumns] = useState<{ + mobile?: string; + from?: string; + alias?: string; + }>({}); + + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) { + return; + } + + // 重置状态 + setError(null); + setIsImportSuccessful(false); + 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; + } + + let workbook; + try { + workbook = XLSX.read(data, { type: "binary" }); + } catch (parseErr) { + console.error("解析Excel内容失败:", parseErr); + 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, { header: "A" }); + + // 检查是否有数据 + 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) { + setError("Excel 文件的第一行为空,无法识别栏位"); + 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")) { + mobileColumn = key; + } 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("账号")) { + aliasColumn = key; + } + } + + // 保存检测到的列名 + if (mobileColumn && firstRow[mobileColumn]) { + setDetectedColumns(prev => ({ ...prev, mobile: String(firstRow[mobileColumn]) })); + } + if (fromColumn && firstRow[fromColumn]) { + setDetectedColumns(prev => ({ ...prev, from: String(firstRow[fromColumn]) })); + } + if (aliasColumn && firstRow[aliasColumn]) { + setDetectedColumns(prev => ({ ...prev, alias: String(firstRow[aliasColumn]) })); + } + + if (!mobileColumn) { + 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] === undefined || row[mobileColumn] === null) { + continue; + } + + // 安全地转换并清理手机号 + const mobileStr = row[mobileColumn] !== undefined && row[mobileColumn] !== null + ? String(row[mobileColumn]).trim() + : ""; + + // 过滤非数字字符,确保手机号是纯数字 + const mobile = mobileStr.replace(/\D/g, ""); + + // 手机号为空的跳过 + if (!mobile) continue; + + // 安全地获取来源和别名 + const from = fromColumn && row[fromColumn] !== undefined && row[fromColumn] !== null + ? String(row[fromColumn]).trim() + : ""; + + const alias = aliasColumn && row[aliasColumn] !== undefined && row[aliasColumn] !== null + ? String(row[aliasColumn]).trim() + : ""; + + importedData.push({ + mobile: Number(mobile), + from, + alias + }); + } + + if (importedData.length === 0) { + setError("未找到有效数据,请确保Excel中至少有一行有效的手机号码"); + setIsProcessing(false); + return; + } + + setParsedData(importedData); + setIsProcessing(false); + } catch (err) { + console.error("解析Excel文件出错:", err); + setError("解析Excel文件时出错,请确保文件格式正确"); + setIsProcessing(false); + } + }; + + reader.onerror = () => { + setError("读取文件时出错,请重试"); + setIsProcessing(false); + }; + + reader.readAsBinaryString(file); + }; + + const handleImport = () => { + if (parsedData.length > 0) { + // 打印解析数据 + console.log("导入的联系人数据:"); + console.log(JSON.stringify(parsedData, null, 2)); + + // 在界面上显示成功消息 + setIsImportSuccessful(true); + } + }; + + const handleReset = () => { + setParsedData([]); + setFileName(""); + setError(null); + setIsImportSuccessful(false); + const fileInput = document.getElementById("file-input") as HTMLInputElement; + if (fileInput) { + fileInput.value = ""; + } + }; + + return ( +
+ +

联系人导入

+ +
+
+ + + {fileName && ( +

+ 当前文件: {fileName} +

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

正在处理Excel文件...

+
+ )} + + {error && ( + + + {error} + + )} + + {isImportSuccessful && ( + + + 已成功导入 {parsedData.length} 条联系人数据!数据已打印到控制台。 + + + )} + + {parsedData.length > 0 && !isImportSuccessful && ( +
+

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

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

检测到的列名:

+
    + {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} 条

} +
+
+
+

数据结构:

+
+
{`[
+  {
+    "mobile": 13800000000,
+    "from": "小红书",
+    "alias": "xxxxxx"
+  },
+  ...
+]`}
+
+
+
+
+ )} + +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/Cunkebao/app/devices/page.tsx b/Cunkebao/app/devices/page.tsx index 39a5e808..1ce4af73 100755 --- a/Cunkebao/app/devices/page.tsx +++ b/Cunkebao/app/devices/page.tsx @@ -464,11 +464,11 @@ export default function DevicesPage() { // 删除后刷新列表 if (successCount > 0) { - toast({ - title: "批量删除成功", + toast({ + title: "批量删除成功", description: `已删除 ${successCount} 个设备`, - }) - setSelectedDevices([]) + }) + setSelectedDevices([]) handleRefresh() } else { toast({ @@ -628,7 +628,7 @@ export default function DevicesPage() { {isLoading &&
加载中...
} {!hasMore && devices.length > 0 &&
没有更多设备了
} {!hasMore && devices.length === 0 &&
暂无设备
} - + @@ -731,7 +731,7 @@ export default function DevicesPage() {

为设备添加一个便于识别的名称

- +
- + -
- + + diff --git a/Cunkebao/app/excel-import-demo/page.tsx b/Cunkebao/app/excel-import-demo/page.tsx new file mode 100644 index 00000000..99642d16 --- /dev/null +++ b/Cunkebao/app/excel-import-demo/page.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React, { useState } from "react"; +import { ExcelImporter } from "@/components/ExcelImporter"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Code } from "@/components/ui/code"; +import { Separator } from "@/components/ui/separator"; + +interface ImportedData { + mobile: number; + from: string; + alias: string; +} + +export default function ExcelImportDemo() { + const [importedData, setImportedData] = useState([]); + const [showData, setShowData] = useState(false); + + const handleImport = (data: ImportedData[]) => { + setImportedData(data); + setShowData(true); + console.log("导入的数据:", data); + }; + + return ( +
+
+

Excel导入演示

+

+ 本演示页面用于测试Excel文件导入功能,将解析Excel中的手机号码、来源和微信号字段。 +

+
+ + + +
+
+

上传Excel文件

+ +
+ +
+

导入结果

+ + {importedData.length > 0 ? ( +
+

导入 {importedData.length} 条数据

+
+
+                    {JSON.stringify(importedData, null, 2)}
+                  
+
+
+ +
+
+ ) : ( +
+

尚未导入数据

+

请上传Excel文件并点击确认导入

+
+ )} +
+
+
+ + + +
+

使用说明

+
+ +

功能说明

+
    +
  • 支持上传.xlsx和.xls格式的Excel文件
  • +
  • 自动识别"手机号码"、"来源"和"微信号"栏位
  • +
  • 可以自动跳过没有手机号的行
  • +
  • 点击"确认导入"按钮将解析后的数据传给回调函数
  • +
+
+ + +

数据结构

+ + {`{ + mobile: "手机号码", // 必填 + from: "来源", // 选填 + alias: "微信号" // 选填 +}`} + +

+ 注意:手机号为空的行会被自动忽略,来源和微信号栏位如果没有数据,则为空字符串。 +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/Cunkebao/app/plans/new/steps/BasicSettings.tsx b/Cunkebao/app/plans/new/steps/BasicSettings.tsx index 7a3b25e7..4655425f 100755 --- a/Cunkebao/app/plans/new/steps/BasicSettings.tsx +++ b/Cunkebao/app/plans/new/steps/BasicSettings.tsx @@ -19,6 +19,8 @@ import { DialogFooter, DialogDescription, } from "@/components/ui/dialog" +import { fetchScenes } from "@/api/scenarios" +import type { SceneItem } from "@/api/scenarios" // 调整场景顺序,确保API获客在最后,并且前三个是最常用的场景 const scenarios = [ @@ -108,6 +110,11 @@ const generatePosterMaterials = (): Material[] => { })) } +// 格式化场景名称,移除"获客"二字 +function formatSceneName(name: string): string { + return name.replace(/获客/g, ""); +} + export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) { const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false) const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false) @@ -123,6 +130,12 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps const [selectedMaterials, setSelectedMaterials] = useState( formData.materials?.length > 0 ? formData.materials : [], ) + + // 添加场景列表状态 + const [scenes, setScenes] = useState([]) + const [loadingScenes, setLoadingScenes] = useState(true) + const [sceneError, setSceneError] = useState(null) + const [showAllScenarios, setShowAllScenarios] = useState(false) const [isImportDialogOpen, setIsImportDialogOpen] = useState(false) const [importedTags, setImportedTags] = useState< @@ -142,6 +155,32 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps questionExtraction: formData.phoneSettings?.questionExtraction ?? true, }) + // 加载场景列表 + useEffect(() => { + const loadScenes = async () => { + try { + setLoadingScenes(true) + setSceneError(null) + + const response = await fetchScenes({ limit: 30 }) + + if (response.code === 200 && response.data?.list) { + setScenes(response.data.list) + } else { + setSceneError(response.msg || "获取场景列表失败") + console.error("获取场景列表失败:", response.msg) + } + } catch (err) { + console.error("获取场景列表失败:", err) + setSceneError("获取场景列表失败,请稍后重试") + } finally { + setLoadingScenes(false) + } + } + + loadScenes() + }, []) + // 初始化时,如果没有选择场景,默认选择海报获客 useEffect(() => { if (!formData.scenario) { @@ -158,16 +197,50 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps } }, [formData, onChange]) + // 处理从API获取的场景选择 + const handleSceneSelect = (scene: SceneItem) => { + // 更新formData中的场景相关数据 + const formattedName = formatSceneName(scene.name); + + onChange({ + ...formData, + sceneId: scene.id, + sceneName: scene.name, + scenario: getLocalScenarioType(scene.name), // 基于名称推断本地场景类型 + }); + + // 如果是电话场景,自动设置计划名称 + if (scene.name.includes("电话")) { + const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); + onChange({ ...formData, planName: `${formattedName}${today}` }); + } + } + + // 处理本地场景选择 const handleScenarioSelect = (scenarioId: string) => { onChange({ ...formData, scenario: scenarioId }) - + // 如果选择了电话获客,自动更新计划名称 if (scenarioId === "phone") { const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "") - onChange({ ...formData, planName: `电话获客${today}` }) + onChange({ ...formData, planName: `电话${today}` }) } } + // 根据场景名称推断本地场景类型 + const getLocalScenarioType = (name: string): string => { + if (name.includes("海报")) return "haibao"; + if (name.includes("订单")) return "order"; + if (name.includes("抖音")) return "douyin"; + if (name.includes("小红书")) return "xiaohongshu"; + if (name.includes("电话")) return "phone"; + if (name.includes("公众号")) return "gongzhonghao"; + if (name.includes("微信群")) return "weixinqun"; + if (name.includes("付款码")) return "payment"; + if (name.includes("API")) return "api"; + return "haibao"; // 默认返回海报获客类型 + } + const handleAccountSelect = (account: Account) => { const updatedAccounts = [...selectedAccounts, account] setSelectedAccounts(updatedAccounts) @@ -217,9 +290,9 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps const [phone, wechat, source, orderAmount, orderDate] = row.split(",") return { phone: phone.trim(), - wechat: wechat.trim(), + wechat: wechat?.trim(), source: source?.trim(), - orderAmount: orderAmount ? Number(orderAmount) : undefined, + orderAmount: orderAmount ? Number(orderAmount) : 0, orderDate: orderDate?.trim(), } }) @@ -234,16 +307,13 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps } 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) + // 直接从 public 目录下载文件 + const link = document.createElement("a") + link.href = "/订单导入模板.xls" + link.download = "订单导入模板.xls" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) } // 处理电话获客设置更新 @@ -258,22 +328,49 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
+ + {/* 场景按钮阵列 */}
- {displayedScenarios.map((scenario) => ( - - ))} + {loadingScenes ? ( + // 加载中状态 + Array.from({ length: 6 }).map((_, index) => ( +
+ )) + ) : sceneError || scenes.length === 0 ? ( + // 加载失败或无数据时显示本地场景 + displayedScenarios.map((scenario) => ( + + )) + ) : ( + // 从API获取的场景列表 + scenes.map((scene) => ( + + )) + )}
- {!showAllScenarios && ( + + {/* 展开更多按钮 - 仅当显示本地场景且未展开全部时显示 */} + {(!loadingScenes && (sceneError || scenes.length === 0) && !showAllScenarios) && ( @@ -491,18 +588,14 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps 电话号码 - 微信号 来源 - 订单金额 {importedTags.slice(0, 5).map((tag, index) => ( {tag.phone} - {tag.wechat} {tag.source} - {tag.orderAmount} ))} {importedTags.length > 5 && ( @@ -672,20 +765,14 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps 电话号码 - 微信号 来源 - 订单金额 - 下单日期 {importedTags.map((tag, index) => ( {tag.phone} - {tag.wechat} {tag.source} - {tag.orderAmount} - {tag.orderDate} ))} diff --git a/Cunkebao/app/plans/new/steps/FriendRequestSettings.tsx b/Cunkebao/app/plans/new/steps/FriendRequestSettings.tsx index 763f725b..c63eba71 100755 --- a/Cunkebao/app/plans/new/steps/FriendRequestSettings.tsx +++ b/Cunkebao/app/plans/new/steps/FriendRequestSettings.tsx @@ -5,13 +5,15 @@ 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 { HelpCircle, MessageSquare, AlertCircle, RefreshCw } 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 { fetchDeviceList } from "@/api/devices" +import type { ServerDevice } from "@/types/device" interface FriendRequestSettingsProps { formData: any @@ -36,20 +38,15 @@ const remarkTypes = [ { 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 || []) + const [selectedDevices, setSelectedDevices] = useState(formData.selectedDevices || []) + const [devices, setDevices] = useState([]) + const [loadingDevices, setLoadingDevices] = useState(false) + const [deviceError, setDeviceError] = useState(null) + const [searchKeyword, setSearchKeyword] = useState("") // 获取场景标题 const getScenarioTitle = () => { @@ -67,6 +64,33 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr } } + // 加载设备列表 + const loadDevices = async () => { + try { + setLoadingDevices(true) + setDeviceError(null) + + const response = await fetchDeviceList(1, 100, searchKeyword) + + if (response.code === 200 && response.data?.list) { + setDevices(response.data.list) + } else { + setDeviceError(response.msg || "获取设备列表失败") + console.error("获取设备列表失败:", response.msg) + } + } catch (err) { + console.error("获取设备列表失败:", err) + setDeviceError("获取设备列表失败,请稍后重试") + } finally { + setLoadingDevices(false) + } + } + + // 初始化时加载设备列表 + useEffect(() => { + loadDevices() + }, []) + // 使用useEffect设置默认值 useEffect(() => { if (!formData.greeting) { @@ -96,7 +120,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr onNext() } - const toggleDeviceSelection = (device: any) => { + const toggleDeviceSelection = (device: ServerDevice) => { const isSelected = selectedDevices.some((d) => d.id === device.id) let newSelectedDevices @@ -110,6 +134,11 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr onChange({ ...formData, selectedDevices: newSelectedDevices }) } + // 根据关键词搜索设备 + const handleSearch = () => { + loadDevices() + } + return (
@@ -128,31 +157,77 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr {isDeviceSelectorOpen && (
- -
- {mockDevices.map((device) => ( -
toggleDeviceSelection(device)} - > -
- d.id === device.id)} - onCheckedChange={() => toggleDeviceSelection(device)} - /> - {device.name} -
- - {device.status === "online" ? "在线" : "离线"} - -
- ))} +
+ setSearchKeyword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> +
+ + {loadingDevices ? ( +
+
+
+ ) : deviceError ? ( +
+ {deviceError} + +
+ ) : devices.length === 0 ? ( +
+ 没有找到设备 +
+ ) : ( +
+ {devices.map((device) => ( +
toggleDeviceSelection(device)} + > +
+ d.id === device.id)} + onCheckedChange={() => toggleDeviceSelection(device)} + /> + {device.memo} +
+ + {device.alive === 1 ? "在线" : "离线"} + +
+ ))} +
+ )}
)}
+ + {selectedDevices.length > 0 && ( +
+ {selectedDevices.map((device) => ( +
+ {device.memo} + +
+ ))} +
+ )}
@@ -239,7 +314,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
{hasWarnings && ( - + 您有未完成的设置项,建议完善后再进入下一步。 diff --git a/Cunkebao/components/ExcelImporter.tsx b/Cunkebao/components/ExcelImporter.tsx new file mode 100644 index 00000000..d0b55979 --- /dev/null +++ b/Cunkebao/components/ExcelImporter.tsx @@ -0,0 +1,341 @@ +"use client"; + +import React, { useState, useRef } from "react"; +import * as XLSX from "xlsx"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; + +export interface ContactData { + mobile: number; + from?: string; + alias?: string; +} + +interface ExcelImporterProps { + onImport: (data: ContactData[]) => void; + onReset?: () => void; +} + +export default function ExcelImporter({ onImport, onReset }: ExcelImporterProps) { + const [parsedData, setParsedData] = useState([]); + const [error, setError] = useState(null); + const [fileName, setFileName] = useState(""); + const [isImportSuccessful, setIsImportSuccessful] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [detectedColumns, setDetectedColumns] = useState<{ + mobile?: string; + from?: string; + alias?: string; + }>({}); + + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) { + return; + } + + // 重置状态 + setError(null); + setIsImportSuccessful(false); + 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' }); + } catch (parseErr) { + console.error("解析Excel内容失败:", parseErr); + 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) { + setError("Excel 文件的第一行为空,无法识别栏位"); + 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")) { + mobileColumn = key; + } 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("账号")) { + aliasColumn = key; + } + } + + // 保存检测到的列名 + if (mobileColumn && firstRow[mobileColumn]) { + setDetectedColumns(prev => ({ ...prev, mobile: String(firstRow[mobileColumn]) })); + } + if (fromColumn && firstRow[fromColumn]) { + setDetectedColumns(prev => ({ ...prev, from: String(firstRow[fromColumn]) })); + } + if (aliasColumn && firstRow[aliasColumn]) { + setDetectedColumns(prev => ({ ...prev, alias: String(firstRow[aliasColumn]) })); + } + + if (!mobileColumn) { + 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') { + mobileNumber = mobileValue; + } else { + // 如果是字符串,去掉非数字字符 + const mobileStr = String(mobileValue).trim().replace(/\D/g, ''); + if (!mobileStr) continue; // 如果手机号为空,跳过该行 + mobileNumber = Number(mobileStr); + if (isNaN(mobileNumber)) continue; // 如果转换后不是数字,跳过该行 + } + + // 构建数据对象 + const contact: ContactData = { + 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) { + console.error("解析Excel文件出错:", err); + setError("解析Excel文件时出错,请确保文件格式正确"); + setIsProcessing(false); + } + }; + + reader.onerror = () => { + setError("读取文件时出错,请重试"); + setIsProcessing(false); + }; + + reader.readAsBinaryString(file); + }; + + const handleImport = () => { + if (parsedData.length > 0) { + onImport(parsedData); + setIsImportSuccessful(true); + } + }; + + const handleReset = () => { + setParsedData([]); + setError(null); + setFileName(""); + setIsImportSuccessful(false); + setDetectedColumns({}); + if (onReset) { + onReset(); + } + }; + + return ( + +
+
+ + + {fileName && ( +

+ 当前文件: {fileName} +

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

正在处理Excel文件...

+
+ )} + + {error && ( + + + {error} + + )} + + {isImportSuccessful && ( + + + 已成功导入 {parsedData.length} 条联系人数据! + + + )} + + {parsedData.length > 0 && !isImportSuccessful && ( +
+

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

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

检测到的列名:

+
    + {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} 条

} +
+
+
+

数据结构:

+
+
{`[
+  {
+    "mobile": 13800000000,
+    "from": "小红书",
+    "alias": "xxxxxx"
+  },
+  ...
+]`}
+
+
+
+
+ )} + +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/Cunkebao/components/ui/code.tsx b/Cunkebao/components/ui/code.tsx new file mode 100644 index 00000000..929c6f1e --- /dev/null +++ b/Cunkebao/components/ui/code.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { cn } from "@/lib/utils"; + +interface CodeProps { + children: React.ReactNode; + className?: string; +} + +export function Code({ children, className }: CodeProps) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/Cunkebao/public/订单导入模板.xls b/Cunkebao/public/订单导入模板.xls new file mode 100644 index 00000000..904eb19c Binary files /dev/null and b/Cunkebao/public/订单导入模板.xls differ diff --git a/Cunkebao/types/xlsx.d.ts b/Cunkebao/types/xlsx.d.ts new file mode 100644 index 00000000..a3700109 --- /dev/null +++ b/Cunkebao/types/xlsx.d.ts @@ -0,0 +1,6 @@ +declare module 'xlsx' { + export function read(data: any, opts?: any): any; + export namespace utils { + export function sheet_to_json(worksheet: any, opts?: any): any[]; + } +} \ No newline at end of file diff --git a/Server/application/http/middleware/JwtAuth.php b/Server/application/common/middleware/jwt.php old mode 100755 new mode 100644 similarity index 96% rename from Server/application/http/middleware/JwtAuth.php rename to Server/application/common/middleware/jwt.php index 7410302a..09331b17 --- a/Server/application/http/middleware/JwtAuth.php +++ b/Server/application/common/middleware/jwt.php @@ -1,5 +1,5 @@ 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer', - 'deleteTime' => 'integer', - 'alive' => 'integer', - 'isDeleted' => 'integer', - 'tenantId' => 'integer', - 'groupId' => 'integer' - ]; - + /** * 获取设备总数 * @param array $where 查询条件 diff --git a/Server/application/devices/model/DeviceHandleLog.php b/Server/application/devices/model/DeviceHandleLog.php index 70ef7b11..5964e23a 100755 --- a/Server/application/devices/model/DeviceHandleLog.php +++ b/Server/application/devices/model/DeviceHandleLog.php @@ -11,27 +11,7 @@ class DeviceHandleLog extends Model { // 设置表名 protected $name = 'device_handle_log'; - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'datetime'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = false; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'userId' => 'integer', - 'deviceId' => 'integer', - 'companyId' => 'integer', - 'createTime' => 'datetime' - ]; - + /** * 添加设备操作日志 * @param array $data 日志数据 diff --git a/Server/application/devices/model/DeviceWechatLogin.php b/Server/application/devices/model/DeviceWechatLogin.php index 138d5cd0..cb7f39e0 100755 --- a/Server/application/devices/model/DeviceWechatLogin.php +++ b/Server/application/devices/model/DeviceWechatLogin.php @@ -11,23 +11,6 @@ class DeviceWechatLogin extends Model // 设置表名 protected $name = 'device_wechat_login'; - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'int'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'deviceId' => 'integer', - 'companyId' => 'integer', - 'createTime' => 'integer' - ]; - /** * 查询设备关联的微信ID列表 * @param int $deviceId 设备ID diff --git a/Server/application/devices/model/FriendTask.php b/Server/application/devices/model/FriendTask.php index 899ad0b6..6b9e47c4 100755 --- a/Server/application/devices/model/FriendTask.php +++ b/Server/application/devices/model/FriendTask.php @@ -14,44 +14,6 @@ class FriendTask extends Model */ protected $table = 'tk_friend_task'; - /** - * 主键 - * @var string - */ - protected $pk = 'id'; - - /** - * 自动写入时间戳 - * @var bool - */ - protected $autoWriteTimestamp = true; - - /** - * 创建时间字段 - * @var string - */ - protected $createTime = 'createTime'; - - /** - * 更新时间字段 - * @var string - */ - protected $updateTime = 'updateTime'; - - /** - * 字段类型 - * @var array - */ - protected $type = [ - 'id' => 'integer', - 'tenantId' => 'integer', - 'operatorAccountId' => 'integer', - 'status' => 'integer', - 'wechatAccountId' => 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer' - ]; - /** * 状态常量 */ diff --git a/Server/application/devices/model/WechatAccount.php b/Server/application/devices/model/WechatAccount.php index 11aaaf78..8f65332f 100755 --- a/Server/application/devices/model/WechatAccount.php +++ b/Server/application/devices/model/WechatAccount.php @@ -12,36 +12,6 @@ class WechatAccount extends Model // 设置表名 protected $name = 'wechat_account'; - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'datetime'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = 'updateTime'; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'deviceAccountId' => 'integer', - 'keFuAlive' => 'integer', - 'deviceAlive' => 'integer', - 'wechatAlive' => 'integer', - 'yesterdayMsgCount' => 'integer', - 'sevenDayMsgCount' => 'integer', - 'thirtyDayMsgCount' => 'integer', - 'totalFriend' => 'integer', - 'maleFriend' => 'integer', - 'femaleFriend' => 'integer', - 'gender' => 'integer', - 'currentDeviceId' => 'integer', - 'isDeleted' => 'integer', - 'groupId' => 'integer', - 'status' => 'integer' - ]; - /** * 获取在线微信账号数量 * diff --git a/Server/application/devices/model/WechatFriend.php b/Server/application/devices/model/WechatFriend.php index c166d922..a9fdb751 100644 --- a/Server/application/devices/model/WechatFriend.php +++ b/Server/application/devices/model/WechatFriend.php @@ -11,34 +11,7 @@ class WechatFriend extends Model { // 设置表名 protected $name = 'wechat_friend'; - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'datetime'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = 'updateTime'; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'wechatAccountId' => 'integer', - 'gender' => 'integer', - 'addFrom' => 'integer', - 'isDeleted' => 'integer', - 'isPassed' => 'integer', - 'accountId' => 'integer', - 'groupId' => 'integer', - 'labels' => 'json', - 'deleteTime' => 'datetime', - 'passTime' => 'datetime', - 'createTime' => 'datetime' - ]; - + /** * 根据微信账号ID获取好友列表 * diff --git a/Server/application/http/middleware/jwt.php b/Server/application/http/middleware/jwt.php deleted file mode 100755 index 0be8c589..00000000 --- a/Server/application/http/middleware/jwt.php +++ /dev/null @@ -1,11 +0,0 @@ -middleware(['jwt']); \ No newline at end of file diff --git a/Server/application/plan/controller/Tag.php b/Server/application/plan/controller/Tag.php deleted file mode 100644 index 12a0ff8d..00000000 --- a/Server/application/plan/controller/Tag.php +++ /dev/null @@ -1,247 +0,0 @@ - 200, - 'msg' => '获取成功', - 'data' => $tags - ]); - } - - /** - * 创建标签 - * - * @return \think\response\Json - */ - public function save() - { - $data = Request::post(); - - // 数据验证 - if (empty($data['name']) || empty($data['type'])) { - return json([ - 'code' => 400, - 'msg' => '缺少必要参数' - ]); - } - - try { - // 创建或获取标签 - $tagId = TagModel::getOrCreate($data['name'], $data['type']); - - return json([ - 'code' => 200, - 'msg' => '创建成功', - 'data' => $tagId - ]); - - } catch (\Exception $e) { - Log::error('创建标签异常', [ - 'data' => $data, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return json([ - 'code' => 500, - 'msg' => '创建失败:' . $e->getMessage() - ]); - } - } - - /** - * 批量创建标签 - * - * @return \think\response\Json - */ - public function batchCreate() - { - $data = Request::post(); - - // 数据验证 - if (empty($data['names']) || empty($data['type'])) { - return json([ - 'code' => 400, - 'msg' => '缺少必要参数' - ]); - } - - // 检查名称数组 - if (!is_array($data['names'])) { - return json([ - 'code' => 400, - 'msg' => '标签名称必须是数组' - ]); - } - - try { - $result = []; - - // 批量处理标签 - foreach ($data['names'] as $name) { - $name = trim($name); - if (empty($name)) continue; - - $tagId = TagModel::getOrCreate($name, $data['type']); - $result[] = [ - 'id' => $tagId, - 'name' => $name, - 'type' => $data['type'] - ]; - } - - return json([ - 'code' => 200, - 'msg' => '创建成功', - 'data' => $result - ]); - - } catch (\Exception $e) { - Log::error('批量创建标签异常', [ - 'data' => $data, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return json([ - 'code' => 500, - 'msg' => '创建失败:' . $e->getMessage() - ]); - } - } - - /** - * 更新标签 - * - * @param int $id - * @return \think\response\Json - */ - public function update($id) - { - $data = Request::put(); - - // 检查标签是否存在 - $tag = TagModel::get($id); - if (!$tag) { - return json([ - 'code' => 404, - 'msg' => '标签不存在' - ]); - } - - // 准备更新数据 - $updateData = []; - - // 只允许更新特定字段 - $allowedFields = ['name', 'status']; - foreach ($allowedFields as $field) { - if (isset($data[$field])) { - $updateData[$field] = $data[$field]; - } - } - - // 更新标签 - $tag->save($updateData); - - // 如果更新了标签名称,且该标签有使用次数,则增加计数 - if (isset($updateData['name']) && $updateData['name'] != $tag->name && $tag->count > 0) { - $tag->updateCount(1); - } - - return json([ - 'code' => 200, - 'msg' => '更新成功' - ]); - } - - /** - * 删除标签 - * - * @param int $id - * @return \think\response\Json - */ - public function delete($id) - { - // 检查标签是否存在 - $tag = TagModel::get($id); - if (!$tag) { - return json([ - 'code' => 404, - 'msg' => '标签不存在' - ]); - } - - // 更新状态为删除 - $tag->save([ - 'status' => 0 - ]); - - return json([ - 'code' => 200, - 'msg' => '删除成功' - ]); - } - - /** - * 获取标签名称 - * - * @return \think\response\Json - */ - public function getNames() - { - $ids = Request::param('ids'); - - // 验证参数 - if (empty($ids)) { - return json([ - 'code' => 400, - 'msg' => '缺少标签ID参数' - ]); - } - - // 处理参数 - if (is_string($ids)) { - $ids = explode(',', $ids); - } - - // 获取标签名称 - $names = TagModel::getTagNames($ids); - - return json([ - 'code' => 200, - 'msg' => '获取成功', - 'data' => $names - ]); - } -} \ No newline at end of file diff --git a/Server/application/plan/controller/Traffic.php b/Server/application/plan/controller/Traffic.php deleted file mode 100644 index 378c1bee..00000000 --- a/Server/application/plan/controller/Traffic.php +++ /dev/null @@ -1,380 +0,0 @@ - 200, - 'msg' => '获取成功', - 'data' => $result - ]); - } - - /** - * 获取流量详情 - * - * @param int $id - * @return \think\response\Json - */ - public function read($id) - { - $traffic = TrafficPool::get($id); - if (!$traffic) { - return json([ - 'code' => 404, - 'msg' => '流量记录不存在' - ]); - } - - // 获取流量来源 - $sources = TrafficSource::getSourcesByTrafficId($id); - - return json([ - 'code' => 200, - 'msg' => '获取成功', - 'data' => [ - 'traffic' => $traffic, - 'sources' => $sources - ] - ]); - } - - /** - * 创建或更新流量 - * - * @return \think\response\Json - */ - public function save() - { - $data = Request::post(); - - // 数据验证 - $validate = validate('app\plan\validate\Traffic'); - if (!$validate->check($data)) { - return json([ - 'code' => 400, - 'msg' => $validate->getError() - ]); - } - - try { - // 添加或更新流量 - $result = TrafficPool::addOrUpdateTraffic( - $data['mobile'], - $data['gender'] ?? 0, - $data['age'] ?? 0, - $data['tags'] ?? '', - $data['province'] ?? '', - $data['city'] ?? '', - $data['source_channel'] ?? '', - $data['source_detail'] ?? [] - ); - - return json([ - 'code' => 200, - 'msg' => '保存成功', - 'data' => $result - ]); - - } catch (\Exception $e) { - Log::error('保存流量记录异常', [ - 'data' => $data, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return json([ - 'code' => 500, - 'msg' => '保存失败:' . $e->getMessage() - ]); - } - } - - /** - * 更新流量记录 - * - * @param int $id - * @return \think\response\Json - */ - public function update($id) - { - $data = Request::put(); - - // 检查流量记录是否存在 - $traffic = TrafficPool::get($id); - if (!$traffic) { - return json([ - 'code' => 404, - 'msg' => '流量记录不存在' - ]); - } - - // 准备更新数据 - $updateData = []; - - // 只允许更新特定字段 - $allowedFields = ['gender', 'age', 'tags', 'province', 'city', 'status']; - foreach ($allowedFields as $field) { - if (isset($data[$field])) { - $updateData[$field] = $data[$field]; - } - } - - // 更新流量记录 - $traffic->save($updateData); - - return json([ - 'code' => 200, - 'msg' => '更新成功' - ]); - } - - /** - * 删除流量记录 - * - * @param int $id - * @return \think\response\Json - */ - public function delete($id) - { - // 检查流量记录是否存在 - $traffic = TrafficPool::get($id); - if (!$traffic) { - return json([ - 'code' => 404, - 'msg' => '流量记录不存在' - ]); - } - - // 更新状态为无效 - $traffic->save([ - 'status' => 0 - ]); - - return json([ - 'code' => 200, - 'msg' => '删除成功' - ]); - } - - /** - * 获取流量来源统计 - * - * @return \think\response\Json - */ - public function sourceStats() - { - $channel = Request::param('channel', ''); - $planId = Request::param('plan_id', 0, 'intval'); - $sceneId = Request::param('scene_id', 0, 'intval'); - $startDate = Request::param('start_date', '', 'trim'); - $endDate = Request::param('end_date', '', 'trim'); - - // 获取统计数据 - $stats = TrafficSource::getSourceStats($channel, $planId, $sceneId, $startDate, $endDate); - - return json([ - 'code' => 200, - 'msg' => '获取成功', - 'data' => $stats - ]); - } - - /** - * 处理外部流量 - * - * @return \think\response\Json - */ - public function handleExternalTraffic() - { - $data = Request::post(); - - // 验证必要参数 - if (empty($data['scene_id']) || empty($data['mobile'])) { - return json([ - 'code' => 400, - 'msg' => '缺少必要参数' - ]); - } - - try { - // 获取场景处理器 - $handler = SceneHandler::getHandler($data['scene_id']); - - // 根据场景类型处理流量 - switch ($data['scene_type'] ?? '') { - case 'poster': - $result = $handler->handlePosterScan($data['mobile'], $data); - break; - - case 'order': - $result = $handler->handleOrderImport($data['orders'] ?? []); - break; - - default: - $result = $handler->handleChannelTraffic($data['mobile'], $data['channel'] ?? '', $data); - } - - return json([ - 'code' => 200, - 'msg' => '处理成功', - 'data' => $result - ]); - - } catch (\Exception $e) { - Log::error('处理外部流量异常', [ - 'data' => $data, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return json([ - 'code' => 500, - 'msg' => '处理失败:' . $e->getMessage() - ]); - } - } - - /** - * 批量导入流量 - * - * @return \think\response\Json - */ - public function importTraffic() - { - // 检查是否上传了文件 - $file = Request::file('file'); - if (!$file) { - return json([ - 'code' => 400, - 'msg' => '未上传文件' - ]); - } - - // 检查文件类型,只允许csv或xlsx - $fileExt = strtolower($file->getOriginalExtension()); - if (!in_array($fileExt, ['csv', 'xlsx'])) { - return json([ - 'code' => 400, - 'msg' => '仅支持CSV或XLSX格式文件' - ]); - } - - try { - // 处理上传文件 - $saveName = \think\facade\Filesystem::disk('upload')->putFile('traffic', $file); - $filePath = app()->getRuntimePath() . 'storage/upload/' . $saveName; - - // 读取文件内容并导入 - $results = []; - $success = 0; - $fail = 0; - - // 这里简化处理,实际应当使用专业的Excel/CSV解析库 - if ($fileExt == 'csv') { - $handle = fopen($filePath, 'r'); - - // 跳过标题行 - fgetcsv($handle); - - while (($data = fgetcsv($handle)) !== false) { - if (count($data) < 1) continue; - - $mobile = trim($data[0]); - // 验证手机号 - if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) { - $fail++; - continue; - } - - // 添加或更新流量 - TrafficPool::addOrUpdateTraffic( - $mobile, - isset($data[1]) ? intval($data[1]) : 0, // 性别 - isset($data[2]) ? intval($data[2]) : 0, // 年龄 - isset($data[3]) ? $data[3] : '', // 标签 - isset($data[4]) ? $data[4] : '', // 省份 - isset($data[5]) ? $data[5] : '', // 城市 - 'import', // 来源渠道 - ['detail' => '批量导入'] // 来源详情 - ); - - $success++; - } - - fclose($handle); - } else { - // 处理xlsx文件,实际应当使用专业的Excel解析库 - // 此处代码省略,依赖于具体的Excel解析库 - } - - return json([ - 'code' => 200, - 'msg' => '导入完成', - 'data' => [ - 'success' => $success, - 'fail' => $fail - ] - ]); - - } catch (\Exception $e) { - Log::error('批量导入流量异常', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return json([ - 'code' => 500, - 'msg' => '导入失败:' . $e->getMessage() - ]); - } - } -} \ No newline at end of file diff --git a/Server/application/plan/controller/TrafficTag.php b/Server/application/plan/controller/TrafficTag.php new file mode 100644 index 00000000..8f890d47 --- /dev/null +++ b/Server/application/plan/controller/TrafficTag.php @@ -0,0 +1,64 @@ +userInfo; + + // 获取查询条件 + $where = []; + + // 关键词搜索 + $keyword = Request::param('keyword', ''); + if (!empty($keyword)) { + $where[] = ['tagName', 'like', "%{$keyword}%"]; + } + + // 添加公司ID过滤条件 + $where[] = ['companyId', '=', $userInfo['companyId']]; + + // 获取分页参数 + $page = (int)Request::param('page', 1); + $limit = (int)Request::param('limit', 200); // 默认每页显示200条 + + // 获取排序参数 + $sort = Request::param('sort', 'id'); + $order = Request::param('order', 'desc'); + + // 查询列表 + $list = TrafficTagModel::getTagsByCompany($where, "{$sort} {$order}", $page, $limit); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'total' => $list->total(), + 'list' => $list->items(), + 'page' => $page, + 'limit' => $limit + ] + ]); + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'msg' => '获取失败:' . $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/Server/application/plan/model/PlanExecution.php b/Server/application/plan/model/PlanExecution.php deleted file mode 100644 index e7be622e..00000000 --- a/Server/application/plan/model/PlanExecution.php +++ /dev/null @@ -1,164 +0,0 @@ - 'integer', - 'plan_id' => 'integer', - 'traffic_id' => 'integer', - 'step' => 'integer', - 'status' => 'integer', - 'result' => 'json', - 'start_time' => 'integer', - 'end_time' => 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer' - ]; - - /** - * 状态文本获取器 - * @param int $value 状态值 - * @return string 状态文本 - */ - public function getStatusTextAttr($value, $data) - { - $status = [ - 0 => '等待', - 1 => '进行中', - 2 => '成功', - 3 => '失败' - ]; - return isset($status[$data['status']]) ? $status[$data['status']] : '未知'; - } - - /** - * 步骤文本获取器 - * @param int $value 步骤值 - * @return string 步骤文本 - */ - public function getStepTextAttr($value, $data) - { - $steps = [ - 1 => '基础配置', - 2 => '加友计划', - 3 => 'API调用', - 4 => '标签处理' - ]; - return isset($steps[$data['step']]) ? $steps[$data['step']] : '未知'; - } - - /** - * 创建执行记录 - * @param int $planId 计划ID - * @param int $step 步骤 - * @param array $data 额外数据 - * @return int 新增记录ID - */ - public static function createExecution($planId, $step, $data = []) - { - $model = new self(); - $model->save(array_merge([ - 'plan_id' => $planId, - 'step' => $step, - 'status' => 0, // 等待状态 - 'start_time' => time() - ], $data)); - - return $model->id; - } - - /** - * 更新执行状态 - * @param int $id 记录ID - * @param int $status 状态 - * @param array $data 额外数据 - * @return bool 更新结果 - */ - public static function updateExecution($id, $status, $data = []) - { - $updateData = array_merge([ - 'status' => $status - ], $data); - - // 如果是完成或失败状态,添加结束时间 - if ($status == 2 || $status == 3) { - $updateData['end_time'] = time(); - } - - return self::where('id', $id)->update($updateData); - } - - /** - * 获取计划的执行记录 - * @param int $planId 计划ID - * @param int $step 步骤 - * @return array 执行记录 - */ - public static function getPlanExecutions($planId, $step = null) - { - $where = [ - ['plan_id', '=', $planId] - ]; - - if ($step !== null) { - $where[] = ['step', '=', $step]; - } - - return self::where($where) - ->order('createTime DESC') - ->select(); - } - - /** - * 获取最近的执行记录 - * @param int $planId 计划ID - * @param int $step 步骤 - * @return array|null 执行记录 - */ - public static function getLatestExecution($planId, $step) - { - return self::where([ - ['plan_id', '=', $planId], - ['step', '=', $step] - ]) - ->order('createTime DESC') - ->find(); - } - - /** - * 关联计划 - */ - public function plan() - { - return $this->belongsTo('PlanTask', 'plan_id'); - } - - /** - * 关联流量 - */ - public function traffic() - { - return $this->belongsTo('TrafficPool', 'traffic_id'); - } -} \ No newline at end of file diff --git a/Server/application/plan/model/PlanScene.php b/Server/application/plan/model/PlanScene.php index de84e9e0..14787132 100644 --- a/Server/application/plan/model/PlanScene.php +++ b/Server/application/plan/model/PlanScene.php @@ -10,28 +10,7 @@ class PlanScene extends Model { // 设置表名 protected $name = 'plan_scene'; - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'int'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = 'updateTime'; - protected $deleteTime = 'deleteTime'; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'status' => 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer', - 'deleteTime' => 'integer' - ]; - + /** * 获取场景列表 * @@ -61,15 +40,4 @@ class PlanScene extends Model 'limit' => $limit ]; } - - /** - * 获取单个场景信息 - * - * @param int $id 场景ID - * @return array|null 场景信息 - */ - public static function getSceneInfo($id) - { - return self::where('id', $id)->find(); - } } \ No newline at end of file diff --git a/Server/application/plan/model/PlanTask.php b/Server/application/plan/model/PlanTask.php index df854b42..7b550cd4 100644 --- a/Server/application/plan/model/PlanTask.php +++ b/Server/application/plan/model/PlanTask.php @@ -10,140 +10,4 @@ class PlanTask extends Model { // 设置表名 protected $name = 'plan_task'; - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'int'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = 'updateTime'; - protected $deleteTime = 'deleteTime'; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'device_id' => 'integer', - 'scene_id' => 'integer', - 'scene_config' => 'json', - 'status' => 'integer', - 'current_step' => 'integer', - 'priority' => 'integer', - 'created_by' => 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer', - 'deleteTime' => 'integer' - ]; - - /** - * 状态文本获取器 - * @param int $value 状态值 - * @return string 状态文本 - */ - public function getStatusTextAttr($value, $data) - { - $status = [ - 0 => '停用', - 1 => '启用', - 2 => '完成', - 3 => '失败' - ]; - return isset($status[$data['status']]) ? $status[$data['status']] : '未知'; - } - - /** - * 获取待执行的任务列表 - * @param int $limit 限制数量 - * @return array 任务列表 - */ - public static function getPendingTasks($limit = 10) - { - return self::where('status', 1) - ->order('priority DESC, id ASC') - ->limit($limit) - ->select(); - } - - /** - * 更新任务状态 - * @param int $id 任务ID - * @param int $status 新状态 - * @param int $currentStep 当前步骤 - * @return bool 更新结果 - */ - public static function updateTaskStatus($id, $status, $currentStep = null) - { - $data = ['status' => $status]; - if ($currentStep !== null) { - $data['current_step'] = $currentStep; - } - - return self::where('id', $id)->update($data); - } - - /** - * 获取任务详情 - * @param int $id 任务ID - * @return array|null 任务详情 - */ - public static function getTaskDetail($id) - { - return self::where('id', $id)->find(); - } - - /** - * 获取任务列表 - * @param array $where 查询条件 - * @param string $order 排序 - * @param int $page 页码 - * @param int $limit 每页数量 - * @return array 任务列表和总数 - */ - public static function getTaskList($where = [], $order = 'id desc', $page = 1, $limit = 10) - { - // 构建查询 - $query = self::where($where); - - // 计算总数 - $total = $query->count(); - - // 分页查询数据 - $list = $query->page($page, $limit) - ->order($order) - ->select(); - - return [ - 'list' => $list, - 'total' => $total, - 'page' => $page, - 'limit' => $limit - ]; - } - - /** - * 关联场景 - */ - public function scene() - { - return $this->belongsTo('PlanScene', 'scene_id'); - } - - /** - * 关联设备 - */ - public function device() - { - return $this->belongsTo('app\devices\model\Device', 'device_id'); - } - - /** - * 关联执行记录 - */ - public function executions() - { - return $this->hasMany('PlanExecution', 'plan_id'); - } } \ No newline at end of file diff --git a/Server/application/plan/model/Tag.php b/Server/application/plan/model/Tag.php deleted file mode 100644 index 01813874..00000000 --- a/Server/application/plan/model/Tag.php +++ /dev/null @@ -1,125 +0,0 @@ - 'integer', - 'count' => 'integer', - 'status' => 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer' - ]; - - /** - * 获取或创建标签 - * @param string $name 标签名 - * @param string $type 标签类型 - * @param string $color 标签颜色 - * @return int 标签ID - */ - public static function getOrCreate($name, $type = 'traffic', $color = '') - { - $tag = self::where([ - ['name', '=', $name], - ['type', '=', $type] - ])->find(); - - if ($tag) { - return $tag['id']; - } else { - $model = new self(); - $model->save([ - 'name' => $name, - 'type' => $type, - 'color' => $color ?: self::getRandomColor(), - 'count' => 0, - 'status' => 1 - ]); - return $model->id; - } - } - - /** - * 更新标签使用次数 - * @param int $id 标签ID - * @param int $increment 增量 - * @return bool 更新结果 - */ - public static function updateCount($id, $increment = 1) - { - return self::where('id', $id)->inc('count', $increment)->update(); - } - - /** - * 获取标签列表 - * @param string $type 标签类型 - * @param array $where 额外条件 - * @return array 标签列表 - */ - public static function getTagsByType($type = 'traffic', $where = []) - { - $conditions = array_merge([ - ['type', '=', $type], - ['status', '=', 1] - ], $where); - - return self::where($conditions) - ->order('count DESC, id DESC') - ->select(); - } - - /** - * 根据ID获取标签名称 - * @param array $ids 标签ID数组 - * @return array 标签名称数组 - */ - public static function getTagNames($ids) - { - if (empty($ids)) { - return []; - } - - $tagIds = is_array($ids) ? $ids : explode(',', $ids); - - $tags = self::where('id', 'in', $tagIds)->column('name'); - - return $tags; - } - - /** - * 获取随机颜色 - * @return string 颜色代码 - */ - private static function getRandomColor() - { - $colors = [ - '#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', - '#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4caf50', - '#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', - '#ff5722', '#795548', '#9e9e9e', '#607d8b' - ]; - - return $colors[array_rand($colors)]; - } -} \ No newline at end of file diff --git a/Server/application/plan/model/TrafficPool.php b/Server/application/plan/model/TrafficPool.php index 617b23b3..ff971597 100644 --- a/Server/application/plan/model/TrafficPool.php +++ b/Server/application/plan/model/TrafficPool.php @@ -10,129 +10,4 @@ class TrafficPool extends Model { // 设置表名 protected $name = 'traffic_pool'; - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'int'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = 'updateTime'; - protected $deleteTime = 'deleteTime'; - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'gender' => 'integer', - 'age' => 'integer', - 'status' => 'integer', - 'last_used_time' => 'integer', - 'createTime' => 'integer', - 'updateTime' => 'integer', - 'deleteTime' => 'integer' - ]; - - /** - * 添加或更新流量信息 - * @param string $mobile 手机号 - * @param array $data 流量数据 - * @return int|bool 流量ID或更新结果 - */ - public static function addOrUpdateTraffic($mobile, $data = []) - { - // 查询是否已存在该手机号 - $exists = self::where('mobile', $mobile)->find(); - - // 设置通用数据 - $saveData = array_merge([ - 'mobile' => $mobile, - 'status' => 1, - 'last_used_time' => time() - ], $data); - - if ($exists) { - // 更新已存在的流量记录 - return self::where('id', $exists['id'])->update($saveData); - } else { - // 创建新的流量记录 - $model = new self(); - $model->save($saveData); - return $model->id; - } - } - - /** - * 获取可用的流量列表 - * @param array $where 查询条件 - * @param string $order 排序 - * @param int $page 页码 - * @param int $limit 每页数量 - * @return array 流量列表和总数 - */ - public static function getAvailableTraffic($where = [], $order = 'last_used_time ASC', $page = 1, $limit = 10) - { - // 确保只查询有效流量 - $where[] = ['status', '=', 1]; - - // 构建查询 - $query = self::where($where); - - // 计算总数 - $total = $query->count(); - - // 分页查询数据 - $list = $query->page($page, $limit) - ->order($order) - ->select(); - - return [ - 'list' => $list, - 'total' => $total, - 'page' => $page, - 'limit' => $limit - ]; - } - - /** - * 设置流量使用时间 - * @param int $id 流量ID - * @return bool 更新结果 - */ - public static function setTrafficUsed($id) - { - return self::where('id', $id)->update([ - 'last_used_time' => time() - ]); - } - - /** - * 获取流量详情 - * @param int $id 流量ID - * @return array|null 流量详情 - */ - public static function getTrafficDetail($id) - { - return self::where('id', $id)->find(); - } - - /** - * 根据手机号获取流量详情 - * @param string $mobile 手机号 - * @return array|null 流量详情 - */ - public static function getTrafficByMobile($mobile) - { - return self::where('mobile', $mobile)->find(); - } - - /** - * 关联流量来源 - */ - public function sources() - { - return $this->hasMany('TrafficSource', 'traffic_id'); - } } \ No newline at end of file diff --git a/Server/application/plan/model/TrafficSource.php b/Server/application/plan/model/TrafficSource.php index 4d3d6528..830baedb 100644 --- a/Server/application/plan/model/TrafficSource.php +++ b/Server/application/plan/model/TrafficSource.php @@ -10,142 +10,4 @@ class TrafficSource extends Model { // 设置表名 protected $name = 'traffic_source'; - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = 'int'; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = false; // 没有更新时间字段 - - // 定义字段类型 - protected $type = [ - 'id' => 'integer', - 'traffic_id' => 'integer', - 'plan_id' => 'integer', - 'scene_id' => 'integer', - 'source_detail' => 'json', - 'createTime' => 'integer' - ]; - - /** - * 渠道文本获取器 - * @param string $value 渠道值 - * @return string 渠道文本 - */ - public function getChannelTextAttr($value, $data) - { - $channels = [ - 'poster' => '海报', - 'order' => '订单', - 'douyin' => '抖音', - 'xiaohongshu' => '小红书', - 'phone' => '电话', - 'wechat' => '公众号', - 'group' => '微信群', - 'payment' => '付款码', - 'api' => 'API接口' - ]; - - return isset($channels[$data['channel']]) ? $channels[$data['channel']] : '未知'; - } - - /** - * 添加流量来源记录 - * @param int $trafficId 流量ID - * @param string $channel 渠道 - * @param array $data 额外数据 - * @return int 新增记录ID - */ - public static function addSource($trafficId, $channel, $data = []) - { - $model = new self(); - $model->save(array_merge([ - 'traffic_id' => $trafficId, - 'channel' => $channel, - 'ip' => request()->ip(), - 'user_agent' => request()->header('user-agent') - ], $data)); - - return $model->id; - } - - /** - * 获取流量来源列表 - * @param int $trafficId 流量ID - * @return array 来源列表 - */ - public static function getSourcesByTrafficId($trafficId) - { - return self::where('traffic_id', $trafficId) - ->order('createTime DESC') - ->select(); - } - - /** - * 获取来源统计 - * @param string $channel 渠道 - * @param int $planId 计划ID - * @param int $sceneId 场景ID - * @param string $startTime 开始时间 - * @param string $endTime 结束时间 - * @return array 统计数据 - */ - public static function getSourceStats($channel = null, $planId = null, $sceneId = null, $startTime = null, $endTime = null) - { - $where = []; - - if ($channel !== null) { - $where[] = ['channel', '=', $channel]; - } - - if ($planId !== null) { - $where[] = ['plan_id', '=', $planId]; - } - - if ($sceneId !== null) { - $where[] = ['scene_id', '=', $sceneId]; - } - - if ($startTime !== null) { - $where[] = ['createTime', '>=', strtotime($startTime)]; - } - - if ($endTime !== null) { - $where[] = ['createTime', '<=', strtotime($endTime)]; - } - - return self::where($where) - ->field('channel, COUNT(*) as count') - ->group('channel') - ->select(); - } - - /** - * 关联流量 - */ - public function traffic() - { - return $this->belongsTo('TrafficPool', 'traffic_id'); - } - - /** - * 关联计划 - */ - public function plan() - { - return $this->belongsTo('PlanTask', 'plan_id'); - } - - /** - * 关联场景 - */ - public function scene() - { - return $this->belongsTo('PlanScene', 'scene_id'); - } } \ No newline at end of file diff --git a/Server/application/plan/model/TrafficTag.php b/Server/application/plan/model/TrafficTag.php new file mode 100644 index 00000000..ab76eb43 --- /dev/null +++ b/Server/application/plan/model/TrafficTag.php @@ -0,0 +1,32 @@ +where('deleteTime', 0) // 只查询未删除的记录 + ->order($order) + ->paginate($limit, false, [ + 'page' => $page + ]); + } +} \ No newline at end of file diff --git a/Server/application/plan/route.php b/Server/application/plan/route.php deleted file mode 100644 index 0b3964d5..00000000 --- a/Server/application/plan/route.php +++ /dev/null @@ -1,30 +0,0 @@ - 处理器名称 - 1 => 'PosterScene', - 2 => 'OrderScene', - 3 => 'DouyinScene', - 4 => 'XiaohongshuScene', - 5 => 'PhoneScene', - 6 => 'WechatScene', - 7 => 'GroupScene', - 8 => 'PaymentScene', - 9 => 'ApiScene', - ]; - - if (!isset($handlerMap[$sceneId])) { - throw new Exception('未找到场景处理器'); - } - - $handlerClass = '\\app\\plan\\scene\\' . $handlerMap[$sceneId]; - if (!class_exists($handlerClass)) { - throw new Exception('场景处理器不存在'); - } - - return new $handlerClass($scene); - } - - /** - * 处理海报扫码获客 - * @param string $mobile 手机号 - * @param int $sceneId 场景ID - * @param int $planId 计划ID - * @param array $extra 额外数据 - * @return array 处理结果 - */ - public static function handlePosterScan($mobile, $sceneId, $planId = null, $extra = []) - { - if (empty($mobile)) { - return [ - 'success' => false, - 'message' => '手机号不能为空' - ]; - } - - try { - // 添加或更新流量信息 - $trafficId = TrafficPool::addOrUpdateTraffic($mobile, [ - 'name' => $extra['name'] ?? '', - 'gender' => $extra['gender'] ?? 0, - 'region' => $extra['region'] ?? '' - ]); - - // 添加流量来源记录 - TrafficSource::addSource($trafficId, 'poster', [ - 'plan_id' => $planId, - 'scene_id' => $sceneId, - 'source_detail' => json_encode($extra) - ]); - - return [ - 'success' => true, - 'message' => '海报扫码获客处理成功', - 'data' => [ - 'traffic_id' => $trafficId - ] - ]; - - } catch (Exception $e) { - Log::error('海报扫码获客处理失败', [ - 'mobile' => $mobile, - 'scene_id' => $sceneId, - 'plan_id' => $planId, - 'error' => $e->getMessage() - ]); - - return [ - 'success' => false, - 'message' => '处理失败:' . $e->getMessage() - ]; - } - } - - /** - * 处理订单导入获客 - * @param array $orders 订单数据 - * @param int $sceneId 场景ID - * @param int $planId 计划ID - * @return array 处理结果 - */ - public static function handleOrderImport($orders, $sceneId, $planId = null) - { - if (empty($orders) || !is_array($orders)) { - return [ - 'success' => false, - 'message' => '订单数据格式不正确' - ]; - } - - $success = 0; - $failed = 0; - $errors = []; - - foreach ($orders as $order) { - if (empty($order['mobile'])) { - $failed++; - $errors[] = '订单缺少手机号'; - continue; - } - - try { - // 添加或更新流量信息 - $trafficId = TrafficPool::addOrUpdateTraffic($order['mobile'], [ - 'name' => $order['name'] ?? '', - 'gender' => $order['gender'] ?? 0, - 'region' => $order['region'] ?? '' - ]); - - // 添加流量来源记录 - TrafficSource::addSource($trafficId, 'order', [ - 'plan_id' => $planId, - 'scene_id' => $sceneId, - 'source_detail' => json_encode($order), - 'sub_channel' => $order['order_source'] ?? '' - ]); - - $success++; - - } catch (Exception $e) { - $failed++; - $errors[] = '处理订单失败:' . $e->getMessage(); - - Log::error('订单导入获客处理失败', [ - 'order' => $order, - 'scene_id' => $sceneId, - 'plan_id' => $planId, - 'error' => $e->getMessage() - ]); - } - } - - return [ - 'success' => $success > 0, - 'message' => "导入完成,成功{$success}条,失败{$failed}条", - 'data' => [ - 'success_count' => $success, - 'failed_count' => $failed, - 'errors' => $errors - ] - ]; - } - - /** - * 通用渠道获客处理 - * @param string $mobile 手机号 - * @param string $channel 渠道 - * @param int $sceneId 场景ID - * @param int $planId 计划ID - * @param array $extra 额外数据 - * @return array 处理结果 - */ - public static function handleChannelTraffic($mobile, $channel, $sceneId, $planId = null, $extra = []) - { - if (empty($mobile)) { - return [ - 'success' => false, - 'message' => '手机号不能为空' - ]; - } - - if (empty($channel)) { - return [ - 'success' => false, - 'message' => '渠道不能为空' - ]; - } - - try { - // 添加或更新流量信息 - $trafficId = TrafficPool::addOrUpdateTraffic($mobile, [ - 'name' => $extra['name'] ?? '', - 'gender' => $extra['gender'] ?? 0, - 'region' => $extra['region'] ?? '' - ]); - - // 添加流量来源记录 - TrafficSource::addSource($trafficId, $channel, [ - 'plan_id' => $planId, - 'scene_id' => $sceneId, - 'source_detail' => json_encode($extra), - 'sub_channel' => $extra['sub_channel'] ?? '' - ]); - - return [ - 'success' => true, - 'message' => $channel . '获客处理成功', - 'data' => [ - 'traffic_id' => $trafficId - ] - ]; - - } catch (Exception $e) { - Log::error($channel . '获客处理失败', [ - 'mobile' => $mobile, - 'scene_id' => $sceneId, - 'plan_id' => $planId, - 'error' => $e->getMessage() - ]); - - return [ - 'success' => false, - 'message' => '处理失败:' . $e->getMessage() - ]; - } - } -} \ No newline at end of file diff --git a/Server/application/plan/service/TaskRunner.php b/Server/application/plan/service/TaskRunner.php deleted file mode 100644 index 5abaa558..00000000 --- a/Server/application/plan/service/TaskRunner.php +++ /dev/null @@ -1,675 +0,0 @@ -task = PlanTask::getTaskDetail($task); - } else { - $this->task = $task; - } - - if (empty($this->task)) { - throw new Exception('任务不存在'); - } - - // 注册步骤处理器 - $this->registerStepHandlers(); - } - - /** - * 注册步骤处理器 - */ - protected function registerStepHandlers() - { - // 基础配置 - $this->stepHandlers[1] = function() { - return $this->handleBasicConfig(); - }; - - // 加友计划 - $this->stepHandlers[2] = function() { - return $this->handleAddFriend(); - }; - - // API调用 - $this->stepHandlers[3] = function() { - return $this->handleApiCall(); - }; - - // 标签处理 - $this->stepHandlers[4] = function() { - return $this->handleTagging(); - }; - } - - /** - * 运行任务 - * @return array 执行结果 - */ - public function run() - { - if ($this->task['status'] != 1) { - return [ - 'success' => false, - 'message' => '任务未启用,无法运行' - ]; - } - - // 获取当前步骤 - $currentStep = $this->task['current_step']; - - // 检查是否需要初始化第一步 - if ($currentStep == 0) { - $currentStep = 1; - PlanTask::updateTaskStatus($this->task['id'], 1, $currentStep); - $this->task['current_step'] = $currentStep; - } - - // 执行当前步骤 - if (isset($this->stepHandlers[$currentStep])) { - try { - $result = call_user_func($this->stepHandlers[$currentStep]); - - if ($result['success']) { - // 检查是否需要进入下一步 - if ($result['completed'] && $currentStep < 4) { - $nextStep = $currentStep + 1; - PlanTask::updateTaskStatus($this->task['id'], 1, $nextStep); - } else if ($result['completed'] && $currentStep == 4) { - // 所有步骤已完成,标记任务为完成状态 - PlanTask::updateTaskStatus($this->task['id'], 2, $currentStep); - } - } else { - // 如果步骤执行失败,记录错误并可能更新任务状态 - Log::error('任务执行失败:', [ - 'task_id' => $this->task['id'], - 'step' => $currentStep, - 'error' => $result['message'] - ]); - - // 视情况决定是否将任务标记为失败 - if ($result['fatal']) { - PlanTask::updateTaskStatus($this->task['id'], 3, $currentStep); - } - } - - return $result; - - } catch (Exception $e) { - // 捕获并记录异常 - Log::error('任务执行异常:', [ - 'task_id' => $this->task['id'], - 'step' => $currentStep, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return [ - 'success' => false, - 'message' => '任务执行异常:' . $e->getMessage(), - 'fatal' => true - ]; - } - } else { - return [ - 'success' => false, - 'message' => '未知的任务步骤:' . $currentStep, - 'fatal' => true - ]; - } - } - - /** - * 处理基础配置步骤 - * @return array 处理结果 - */ - protected function handleBasicConfig() - { - // 创建执行记录 - $executionId = PlanExecution::createExecution($this->task['id'], 1, [ - 'status' => 1 // 设置为进行中 - ]); - - try { - // 检查设备状态 - if (empty($this->task['device_id'])) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '未设置设备' - ]); - - return [ - 'success' => false, - 'message' => '未设置设备', - 'fatal' => true - ]; - } - - // 检查场景配置 - if (empty($this->task['scene_id'])) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '未设置获客场景' - ]); - - return [ - 'success' => false, - 'message' => '未设置获客场景', - 'fatal' => true - ]; - } - - // 检查场景配置 - if (empty($this->task['scene_config'])) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '场景配置为空' - ]); - - return [ - 'success' => false, - 'message' => '场景配置为空', - 'fatal' => true - ]; - } - - // 标记基础配置步骤为完成 - PlanExecution::updateExecution($executionId, 2, [ - 'result' => [ - 'device_id' => $this->task['device_id'], - 'scene_id' => $this->task['scene_id'], - 'config_valid' => true - ] - ]); - - return [ - 'success' => true, - 'message' => '基础配置验证通过', - 'completed' => true - ]; - - } catch (Exception $e) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '基础配置异常:' . $e->getMessage() - ]); - - throw $e; - } - } - - /** - * 处理加友计划步骤 - * @return array 处理结果 - */ - protected function handleAddFriend() - { - // 创建执行记录 - $executionId = PlanExecution::createExecution($this->task['id'], 2, [ - 'status' => 1 // 设置为进行中 - ]); - - try { - // 从流量池中选择符合条件的流量 - $trafficConditions = $this->getTrafficConditions(); - $trafficData = TrafficPool::getAvailableTraffic($trafficConditions, 'last_used_time ASC', 1, 1); - - if (empty($trafficData['list'])) { - // 没有符合条件的流量,标记为等待状态 - PlanExecution::updateExecution($executionId, 0, [ - 'error' => '没有符合条件的流量' - ]); - - return [ - 'success' => true, - 'message' => '没有符合条件的流量,等待下次执行', - 'completed' => false // 不算失败,但也不进入下一步 - ]; - } - - $traffic = $trafficData['list'][0]; - - // 调用设备服务执行加好友操作 - $addFriendResult = $this->callDeviceAddFriend($traffic); - - if ($addFriendResult['success']) { - // 更新流量使用状态 - TrafficPool::setTrafficUsed($traffic['id']); - - // 标记执行记录为成功 - PlanExecution::updateExecution($executionId, 2, [ - 'traffic_id' => $traffic['id'], - 'result' => $addFriendResult['data'] - ]); - - return [ - 'success' => true, - 'message' => '加友成功:' . $traffic['mobile'], - 'completed' => true, - 'traffic' => $traffic - ]; - } else { - // 标记执行记录为失败 - PlanExecution::updateExecution($executionId, 3, [ - 'traffic_id' => $traffic['id'], - 'error' => $addFriendResult['message'], - 'result' => $addFriendResult['data'] ?? null - ]); - - return [ - 'success' => false, - 'message' => '加友失败:' . $addFriendResult['message'], - 'fatal' => false // 加友失败不算致命错误,可以下次继续 - ]; - } - - } catch (Exception $e) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '加友计划异常:' . $e->getMessage() - ]); - - throw $e; - } - } - - /** - * 处理API调用步骤 - * @return array 处理结果 - */ - protected function handleApiCall() - { - // 创建执行记录 - $executionId = PlanExecution::createExecution($this->task['id'], 3, [ - 'status' => 1 // 设置为进行中 - ]); - - try { - // 获取上一步成功处理的流量信息 - $lastExecution = PlanExecution::getLatestExecution($this->task['id'], 2); - - if (empty($lastExecution) || $lastExecution['status'] != 2) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '上一步未成功完成' - ]); - - return [ - 'success' => false, - 'message' => '上一步未成功完成,无法进行API调用', - 'fatal' => true - ]; - } - - $trafficId = $lastExecution['traffic_id']; - if (empty($trafficId)) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '未找到有效的流量ID' - ]); - - return [ - 'success' => false, - 'message' => '未找到有效的流量ID', - 'fatal' => true - ]; - } - - // 获取流量详情 - $traffic = TrafficPool::getTrafficDetail($trafficId); - if (empty($traffic)) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '未找到流量信息' - ]); - - return [ - 'success' => false, - 'message' => '未找到流量信息', - 'fatal' => true - ]; - } - - // 根据场景配置调用相应的API - $apiCallResult = $this->callSceneApi($traffic); - - if ($apiCallResult['success']) { - // 标记执行记录为成功 - PlanExecution::updateExecution($executionId, 2, [ - 'traffic_id' => $trafficId, - 'result' => $apiCallResult['data'] - ]); - - return [ - 'success' => true, - 'message' => 'API调用成功', - 'completed' => true, - 'traffic' => $traffic - ]; - } else { - // 标记执行记录为失败 - PlanExecution::updateExecution($executionId, 3, [ - 'traffic_id' => $trafficId, - 'error' => $apiCallResult['message'], - 'result' => $apiCallResult['data'] ?? null - ]); - - return [ - 'success' => false, - 'message' => 'API调用失败:' . $apiCallResult['message'], - 'fatal' => $apiCallResult['fatal'] ?? false - ]; - } - - } catch (Exception $e) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => 'API调用异常:' . $e->getMessage() - ]); - - throw $e; - } - } - - /** - * 处理标签步骤 - * @return array 处理结果 - */ - protected function handleTagging() - { - // 创建执行记录 - $executionId = PlanExecution::createExecution($this->task['id'], 4, [ - 'status' => 1 // 设置为进行中 - ]); - - try { - // 获取上一步成功处理的流量信息 - $lastExecution = PlanExecution::getLatestExecution($this->task['id'], 3); - - if (empty($lastExecution) || $lastExecution['status'] != 2) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '上一步未成功完成' - ]); - - return [ - 'success' => false, - 'message' => '上一步未成功完成,无法进行标签处理', - 'fatal' => true - ]; - } - - $trafficId = $lastExecution['traffic_id']; - if (empty($trafficId)) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '未找到有效的流量ID' - ]); - - return [ - 'success' => false, - 'message' => '未找到有效的流量ID', - 'fatal' => true - ]; - } - - // 获取流量详情 - $traffic = TrafficPool::getTrafficDetail($trafficId); - if (empty($traffic)) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '未找到流量信息' - ]); - - return [ - 'success' => false, - 'message' => '未找到流量信息', - 'fatal' => true - ]; - } - - // 获取并应用标签 - $taggingResult = $this->applyTags($traffic); - - if ($taggingResult['success']) { - // 标记执行记录为成功 - PlanExecution::updateExecution($executionId, 2, [ - 'traffic_id' => $trafficId, - 'result' => $taggingResult['data'] - ]); - - return [ - 'success' => true, - 'message' => '标签处理成功', - 'completed' => true, - 'traffic' => $traffic - ]; - } else { - // 标记执行记录为失败 - PlanExecution::updateExecution($executionId, 3, [ - 'traffic_id' => $trafficId, - 'error' => $taggingResult['message'], - 'result' => $taggingResult['data'] ?? null - ]); - - return [ - 'success' => false, - 'message' => '标签处理失败:' . $taggingResult['message'], - 'fatal' => $taggingResult['fatal'] ?? false - ]; - } - - } catch (Exception $e) { - PlanExecution::updateExecution($executionId, 3, [ - 'error' => '标签处理异常:' . $e->getMessage() - ]); - - throw $e; - } - } - - /** - * 获取流量筛选条件 - * @return array 条件数组 - */ - protected function getTrafficConditions() - { - $conditions = []; - - // 根据场景配置获取筛选条件 - if (isset($this->task['scene_config']) && is_array($this->task['scene_config'])) { - $config = $this->task['scene_config']; - - // 添加性别筛选 - if (isset($config['gender']) && in_array($config['gender'], [0, 1, 2])) { - $conditions[] = ['gender', '=', $config['gender']]; - } - - // 添加年龄筛选 - if (isset($config['age_min']) && is_numeric($config['age_min'])) { - $conditions[] = ['age', '>=', intval($config['age_min'])]; - } - - if (isset($config['age_max']) && is_numeric($config['age_max'])) { - $conditions[] = ['age', '<=', intval($config['age_max'])]; - } - - // 添加区域筛选 - if (isset($config['region']) && !empty($config['region'])) { - $conditions[] = ['region', 'like', '%' . $config['region'] . '%']; - } - } - - return $conditions; - } - - /** - * 调用设备加好友操作 - * @param array $traffic 流量信息 - * @return array 调用结果 - */ - protected function callDeviceAddFriend($traffic) - { - // 模拟调用设备操作 - // 实际项目中应该调用实际的设备API - - // 记录设备调用日志 - Log::info('设备加好友操作', [ - 'task_id' => $this->task['id'], - 'device_id' => $this->task['device_id'], - 'mobile' => $traffic['mobile'] - ]); - - // 模拟成功率 - $success = mt_rand(0, 10) > 2; - - if ($success) { - return [ - 'success' => true, - 'message' => '加好友操作成功', - 'data' => [ - 'add_time' => date('Y-m-d H:i:s'), - 'device_id' => $this->task['device_id'], - 'mobile' => $traffic['mobile'] - ] - ]; - } else { - return [ - 'success' => false, - 'message' => '加好友操作失败:' . ['设备繁忙', '用户拒绝', '网络异常'][mt_rand(0, 2)], - 'data' => [ - 'attempt_time' => date('Y-m-d H:i:s'), - 'device_id' => $this->task['device_id'], - 'mobile' => $traffic['mobile'] - ] - ]; - } - } - - /** - * 根据场景调用相应API - * @param array $traffic 流量信息 - * @return array 调用结果 - */ - protected function callSceneApi($traffic) - { - // 根据场景类型调用不同API - if (empty($this->task['scene_id'])) { - return [ - 'success' => false, - 'message' => '场景未设置', - 'fatal' => true - ]; - } - - // 记录API调用日志 - Log::info('场景API调用', [ - 'task_id' => $this->task['id'], - 'scene_id' => $this->task['scene_id'], - 'traffic_id' => $traffic['id'] - ]); - - // 模拟成功率 - $success = mt_rand(0, 10) > 1; - - if ($success) { - return [ - 'success' => true, - 'message' => 'API调用成功', - 'data' => [ - 'call_time' => date('Y-m-d H:i:s'), - 'scene_id' => $this->task['scene_id'], - 'traffic_id' => $traffic['id'] - ] - ]; - } else { - return [ - 'success' => false, - 'message' => 'API调用失败:' . ['参数错误', 'API超时', '系统异常'][mt_rand(0, 2)], - 'data' => [ - 'attempt_time' => date('Y-m-d H:i:s'), - 'scene_id' => $this->task['scene_id'], - 'traffic_id' => $traffic['id'] - ], - 'fatal' => false // API调用失败通常不算致命错误 - ]; - } - } - - /** - * 应用标签 - * @param array $traffic 流量信息 - * @return array 处理结果 - */ - protected function applyTags($traffic) - { - // 获取需要应用的标签 - $tags = []; - - // 从场景配置中获取标签 - if (isset($this->task['scene_config']) && is_array($this->task['scene_config']) && isset($this->task['scene_config']['tags'])) { - $configTags = $this->task['scene_config']['tags']; - if (is_array($configTags)) { - $tags = array_merge($tags, $configTags); - } else if (is_string($configTags)) { - $tags[] = $configTags; - } - } - - // 从场景获取标签 - if (!empty($this->task['scene_id'])) { - $tags[] = '场景_' . $this->task['scene_id']; - } - - // 如果没有标签,返回成功 - if (empty($tags)) { - return [ - 'success' => true, - 'message' => '没有需要应用的标签', - 'data' => [] - ]; - } - - // 处理标签 - $tagIds = []; - foreach ($tags as $tagName) { - $tagId = Tag::getOrCreate($tagName, 'friend'); - $tagIds[] = $tagId; - Tag::updateCount($tagId); - } - - // 记录标签应用日志 - Log::info('应用标签', [ - 'task_id' => $this->task['id'], - 'traffic_id' => $traffic['id'], - 'tag_ids' => $tagIds - ]); - - // 更新流量标签 - $existingTags = empty($traffic['tag_ids']) ? [] : explode(',', $traffic['tag_ids']); - $allTags = array_unique(array_merge($existingTags, $tagIds)); - - TrafficPool::where('id', $traffic['id'])->update([ - 'tag_ids' => implode(',', $allTags) - ]); - - return [ - 'success' => true, - 'message' => '标签应用成功', - 'data' => [ - 'tag_ids' => $tagIds, - 'tag_names' => $tags - ] - ]; - } -} \ No newline at end of file diff --git a/Server/application/plan/sql/tables.sql b/Server/application/plan/sql/tables.sql deleted file mode 100644 index 58c2312b..00000000 --- a/Server/application/plan/sql/tables.sql +++ /dev/null @@ -1,92 +0,0 @@ --- 获客计划主表 -CREATE TABLE `tk_plan_task` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', - `name` varchar(100) NOT NULL COMMENT '计划名称', - `device_id` int(10) unsigned DEFAULT NULL COMMENT '关联设备ID', - `scene_id` int(10) unsigned DEFAULT NULL COMMENT '获客场景ID', - `scene_config` text DEFAULT NULL COMMENT '场景配置(JSON格式)', - `status` tinyint(3) unsigned DEFAULT 0 COMMENT '状态:0=停用,1=启用,2=完成,3=失败', - `current_step` tinyint(3) unsigned DEFAULT 0 COMMENT '当前执行步骤', - `priority` tinyint(3) unsigned DEFAULT 5 COMMENT '优先级:1-10,数字越大优先级越高', - `created_by` int(10) unsigned NOT NULL COMMENT '创建人ID', - `createTime` int(11) DEFAULT NULL COMMENT '创建时间', - `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', - `deleteTime` int(11) DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - KEY `idx_status` (`status`), - KEY `idx_device` (`device_id`), - KEY `idx_scene` (`scene_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='获客计划主表'; - --- 流量池表 -CREATE TABLE `tk_traffic_pool` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', - `mobile` varchar(20) NOT NULL COMMENT '手机号', - `name` varchar(50) DEFAULT NULL COMMENT '姓名', - `gender` tinyint(1) DEFAULT NULL COMMENT '性别:0=未知,1=男,2=女', - `age` int(3) DEFAULT NULL COMMENT '年龄', - `region` varchar(100) DEFAULT NULL COMMENT '区域', - `status` tinyint(3) unsigned DEFAULT 1 COMMENT '状态:0=无效,1=有效', - `tag_ids` varchar(255) DEFAULT NULL COMMENT '标签ID,逗号分隔', - `remark` varchar(255) DEFAULT NULL COMMENT '备注', - `last_used_time` int(11) DEFAULT NULL COMMENT '最后使用时间', - `createTime` int(11) DEFAULT NULL COMMENT '创建时间', - `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', - `deleteTime` int(11) DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_mobile` (`mobile`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流量池表'; - --- 流量来源表 -CREATE TABLE `tk_traffic_source` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', - `traffic_id` int(10) unsigned NOT NULL COMMENT '关联流量池ID', - `plan_id` int(10) unsigned DEFAULT NULL COMMENT '关联计划ID', - `scene_id` int(10) unsigned DEFAULT NULL COMMENT '场景ID', - `channel` varchar(50) NOT NULL COMMENT '渠道:poster=海报, order=订单, douyin=抖音, xiaohongshu=小红书, phone=电话, wechat=公众号, group=微信群, payment=付款码, api=API接口', - `sub_channel` varchar(50) DEFAULT NULL COMMENT '子渠道', - `source_detail` text DEFAULT NULL COMMENT '来源详情(JSON格式)', - `ip` varchar(50) DEFAULT NULL COMMENT '来源IP', - `user_agent` varchar(255) DEFAULT NULL COMMENT '用户代理', - `createTime` int(11) DEFAULT NULL COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_traffic` (`traffic_id`), - KEY `idx_plan` (`plan_id`), - KEY `idx_scene` (`scene_id`), - KEY `idx_channel` (`channel`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流量来源表'; - --- 计划执行记录表 -CREATE TABLE `tk_plan_execution` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', - `plan_id` int(10) unsigned NOT NULL COMMENT '关联计划ID', - `traffic_id` int(10) unsigned DEFAULT NULL COMMENT '关联流量ID', - `step` tinyint(3) unsigned NOT NULL COMMENT '执行步骤:1=基础配置,2=加友计划,3=API调用,4=标签处理', - `sub_step` varchar(50) DEFAULT NULL COMMENT '子步骤标识', - `status` tinyint(3) unsigned DEFAULT 0 COMMENT '状态:0=等待,1=进行中,2=成功,3=失败', - `result` text DEFAULT NULL COMMENT '执行结果(JSON格式)', - `error` varchar(255) DEFAULT NULL COMMENT '错误信息', - `start_time` int(11) DEFAULT NULL COMMENT '开始时间', - `end_time` int(11) DEFAULT NULL COMMENT '结束时间', - `createTime` int(11) DEFAULT NULL COMMENT '创建时间', - `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_plan` (`plan_id`), - KEY `idx_traffic` (`traffic_id`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='计划执行记录表'; - --- 标签表 -CREATE TABLE `tk_tag` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', - `name` varchar(50) NOT NULL COMMENT '标签名称', - `color` varchar(20) DEFAULT NULL COMMENT '标签颜色', - `type` varchar(20) DEFAULT 'traffic' COMMENT '标签类型:traffic=流量标签,friend=好友标签', - `count` int(11) DEFAULT 0 COMMENT '使用次数', - `status` tinyint(3) unsigned DEFAULT 1 COMMENT '状态:0=停用,1=启用', - `createTime` int(11) DEFAULT NULL COMMENT '创建时间', - `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_name_type` (`name`, `type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; \ No newline at end of file diff --git a/Server/config/middleware.php b/Server/config/middleware.php index 2edfee13..31f6a822 100755 --- a/Server/config/middleware.php +++ b/Server/config/middleware.php @@ -14,12 +14,7 @@ // +---------------------------------------------------------------------- return [ // 默认中间件命名空间 - 'default_namespace' => 'app\\http\\middleware\\', - - // 别名或分组 - 'alias' => [ - 'jwt' => 'JwtAuth', - ], + 'default_namespace' => 'app\\common\\middleware\\', // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 'priority' => [], diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 00000000..5d0a9aa5 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,47 @@ +{ + "name": "yi-shi", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + } + } +} diff --git a/node_modules/react-dom/LICENSE b/node_modules/react-dom/LICENSE new file mode 100644 index 00000000..b93be905 --- /dev/null +++ b/node_modules/react-dom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/react-dom/README.md b/node_modules/react-dom/README.md new file mode 100644 index 00000000..b078f194 --- /dev/null +++ b/node_modules/react-dom/README.md @@ -0,0 +1,60 @@ +# `react-dom` + +This package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as `react` to npm. + +## Installation + +```sh +npm install react react-dom +``` + +## Usage + +### In the browser + +```js +import { createRoot } from 'react-dom/client'; + +function App() { + return
Hello World
; +} + +const root = createRoot(document.getElementById('root')); +root.render(); +``` + +### On the server + +```js +import { renderToPipeableStream } from 'react-dom/server'; + +function App() { + return
Hello World
; +} + +function handleRequest(res) { + // ... in your server handler ... + const stream = renderToPipeableStream(, { + onShellReady() { + res.statusCode = 200; + res.setHeader('Content-type', 'text/html'); + stream.pipe(res); + }, + // ... + }); +} +``` + +## API + +### `react-dom` + +See https://react.dev/reference/react-dom + +### `react-dom/client` + +See https://react.dev/reference/react-dom/client + +### `react-dom/server` + +See https://react.dev/reference/react-dom/server diff --git a/node_modules/react-dom/cjs/react-dom-client.development.js b/node_modules/react-dom/cjs/react-dom-client.development.js new file mode 100644 index 00000000..dd10829d --- /dev/null +++ b/node_modules/react-dom/cjs/react-dom-client.development.js @@ -0,0 +1,24990 @@ +/** + * @license React + * react-dom-client.development.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + Modernizr 3.0.0pre (Custom Build) | MIT +*/ +"use strict"; +"production" !== process.env.NODE_ENV && + (function () { + function findHook(fiber, id) { + for (fiber = fiber.memoizedState; null !== fiber && 0 < id; ) + (fiber = fiber.next), id--; + return fiber; + } + function copyWithSetImpl(obj, path, index, value) { + if (index >= path.length) return value; + var key = path[index], + updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj); + updated[key] = copyWithSetImpl(obj[key], path, index + 1, value); + return updated; + } + function copyWithRename(obj, oldPath, newPath) { + if (oldPath.length !== newPath.length) + console.warn("copyWithRename() expects paths of the same length"); + else { + for (var i = 0; i < newPath.length - 1; i++) + if (oldPath[i] !== newPath[i]) { + console.warn( + "copyWithRename() expects paths to be the same except for the deepest key" + ); + return; + } + return copyWithRenameImpl(obj, oldPath, newPath, 0); + } + } + function copyWithRenameImpl(obj, oldPath, newPath, index) { + var oldKey = oldPath[index], + updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj); + index + 1 === oldPath.length + ? ((updated[newPath[index]] = updated[oldKey]), + isArrayImpl(updated) + ? updated.splice(oldKey, 1) + : delete updated[oldKey]) + : (updated[oldKey] = copyWithRenameImpl( + obj[oldKey], + oldPath, + newPath, + index + 1 + )); + return updated; + } + function copyWithDeleteImpl(obj, path, index) { + var key = path[index], + updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj); + if (index + 1 === path.length) + return ( + isArrayImpl(updated) ? updated.splice(key, 1) : delete updated[key], + updated + ); + updated[key] = copyWithDeleteImpl(obj[key], path, index + 1); + return updated; + } + function shouldSuspendImpl() { + return !1; + } + function shouldErrorImpl() { + return null; + } + function warnForMissingKey() {} + function warnInvalidHookAccess() { + console.error( + "Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see https://react.dev/link/rules-of-hooks" + ); + } + function warnInvalidContextAccess() { + console.error( + "Context can only be read while React is rendering. In classes, you can read it in the render method or getDerivedStateFromProps. In function components, you can read it directly in the function body, but not inside Hooks like useReducer() or useMemo()." + ); + } + function noop$2() {} + function setToSortedString(set) { + var array = []; + set.forEach(function (value) { + array.push(value); + }); + return array.sort().join(", "); + } + function createFiber(tag, pendingProps, key, mode) { + return new FiberNode(tag, pendingProps, key, mode); + } + function scheduleRoot(root, element) { + root.context === emptyContextObject && + (updateContainerImpl(root.current, 2, element, root, null, null), + flushSyncWork$1()); + } + function scheduleRefresh(root, update) { + if (null !== resolveFamily) { + var staleFamilies = update.staleFamilies; + update = update.updatedFamilies; + flushPendingEffects(); + scheduleFibersWithFamiliesRecursively( + root.current, + update, + staleFamilies + ); + flushSyncWork$1(); + } + } + function setRefreshHandler(handler) { + resolveFamily = handler; + } + function isValidContainer(node) { + return !( + !node || + (1 !== node.nodeType && 9 !== node.nodeType && 11 !== node.nodeType) + ); + } + function getNearestMountedFiber(fiber) { + var node = fiber, + nearestMounted = fiber; + if (fiber.alternate) for (; node.return; ) node = node.return; + else { + fiber = node; + do + (node = fiber), + 0 !== (node.flags & 4098) && (nearestMounted = node.return), + (fiber = node.return); + while (fiber); + } + return 3 === node.tag ? nearestMounted : null; + } + function getSuspenseInstanceFromFiber(fiber) { + if (13 === fiber.tag) { + var suspenseState = fiber.memoizedState; + null === suspenseState && + ((fiber = fiber.alternate), + null !== fiber && (suspenseState = fiber.memoizedState)); + if (null !== suspenseState) return suspenseState.dehydrated; + } + return null; + } + function assertIsMounted(fiber) { + if (getNearestMountedFiber(fiber) !== fiber) + throw Error("Unable to find node on an unmounted component."); + } + function findCurrentFiberUsingSlowPath(fiber) { + var alternate = fiber.alternate; + if (!alternate) { + alternate = getNearestMountedFiber(fiber); + if (null === alternate) + throw Error("Unable to find node on an unmounted component."); + return alternate !== fiber ? null : fiber; + } + for (var a = fiber, b = alternate; ; ) { + var parentA = a.return; + if (null === parentA) break; + var parentB = parentA.alternate; + if (null === parentB) { + b = parentA.return; + if (null !== b) { + a = b; + continue; + } + break; + } + if (parentA.child === parentB.child) { + for (parentB = parentA.child; parentB; ) { + if (parentB === a) return assertIsMounted(parentA), fiber; + if (parentB === b) return assertIsMounted(parentA), alternate; + parentB = parentB.sibling; + } + throw Error("Unable to find node on an unmounted component."); + } + if (a.return !== b.return) (a = parentA), (b = parentB); + else { + for (var didFindChild = !1, _child = parentA.child; _child; ) { + if (_child === a) { + didFindChild = !0; + a = parentA; + b = parentB; + break; + } + if (_child === b) { + didFindChild = !0; + b = parentA; + a = parentB; + break; + } + _child = _child.sibling; + } + if (!didFindChild) { + for (_child = parentB.child; _child; ) { + if (_child === a) { + didFindChild = !0; + a = parentB; + b = parentA; + break; + } + if (_child === b) { + didFindChild = !0; + b = parentB; + a = parentA; + break; + } + _child = _child.sibling; + } + if (!didFindChild) + throw Error( + "Child was not found in either parent set. This indicates a bug in React related to the return pointer. Please file an issue." + ); + } + } + if (a.alternate !== b) + throw Error( + "Return fibers should always be each others' alternates. This error is likely caused by a bug in React. Please file an issue." + ); + } + if (3 !== a.tag) + throw Error("Unable to find node on an unmounted component."); + return a.stateNode.current === a ? fiber : alternate; + } + function findCurrentHostFiberImpl(node) { + var tag = node.tag; + if (5 === tag || 26 === tag || 27 === tag || 6 === tag) return node; + for (node = node.child; null !== node; ) { + tag = findCurrentHostFiberImpl(node); + if (null !== tag) return tag; + node = node.sibling; + } + return null; + } + function getIteratorFn(maybeIterable) { + if (null === maybeIterable || "object" !== typeof maybeIterable) + return null; + maybeIterable = + (MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) || + maybeIterable["@@iterator"]; + return "function" === typeof maybeIterable ? maybeIterable : null; + } + function getComponentNameFromType(type) { + if (null == type) return null; + if ("function" === typeof type) + return type.$$typeof === REACT_CLIENT_REFERENCE + ? null + : type.displayName || type.name || null; + if ("string" === typeof type) return type; + switch (type) { + case REACT_FRAGMENT_TYPE: + return "Fragment"; + case REACT_PROFILER_TYPE: + return "Profiler"; + case REACT_STRICT_MODE_TYPE: + return "StrictMode"; + case REACT_SUSPENSE_TYPE: + return "Suspense"; + case REACT_SUSPENSE_LIST_TYPE: + return "SuspenseList"; + case REACT_ACTIVITY_TYPE: + return "Activity"; + } + if ("object" === typeof type) + switch ( + ("number" === typeof type.tag && + console.error( + "Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue." + ), + type.$$typeof) + ) { + case REACT_PORTAL_TYPE: + return "Portal"; + case REACT_CONTEXT_TYPE: + return (type.displayName || "Context") + ".Provider"; + case REACT_CONSUMER_TYPE: + return (type._context.displayName || "Context") + ".Consumer"; + case REACT_FORWARD_REF_TYPE: + var innerType = type.render; + type = type.displayName; + type || + ((type = innerType.displayName || innerType.name || ""), + (type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef")); + return type; + case REACT_MEMO_TYPE: + return ( + (innerType = type.displayName || null), + null !== innerType + ? innerType + : getComponentNameFromType(type.type) || "Memo" + ); + case REACT_LAZY_TYPE: + innerType = type._payload; + type = type._init; + try { + return getComponentNameFromType(type(innerType)); + } catch (x) {} + } + return null; + } + function getComponentNameFromOwner(owner) { + return "number" === typeof owner.tag + ? getComponentNameFromFiber(owner) + : "string" === typeof owner.name + ? owner.name + : null; + } + function getComponentNameFromFiber(fiber) { + var type = fiber.type; + switch (fiber.tag) { + case 31: + return "Activity"; + case 24: + return "Cache"; + case 9: + return (type._context.displayName || "Context") + ".Consumer"; + case 10: + return (type.displayName || "Context") + ".Provider"; + case 18: + return "DehydratedFragment"; + case 11: + return ( + (fiber = type.render), + (fiber = fiber.displayName || fiber.name || ""), + type.displayName || + ("" !== fiber ? "ForwardRef(" + fiber + ")" : "ForwardRef") + ); + case 7: + return "Fragment"; + case 26: + case 27: + case 5: + return type; + case 4: + return "Portal"; + case 3: + return "Root"; + case 6: + return "Text"; + case 16: + return getComponentNameFromType(type); + case 8: + return type === REACT_STRICT_MODE_TYPE ? "StrictMode" : "Mode"; + case 22: + return "Offscreen"; + case 12: + return "Profiler"; + case 21: + return "Scope"; + case 13: + return "Suspense"; + case 19: + return "SuspenseList"; + case 25: + return "TracingMarker"; + case 1: + case 0: + case 14: + case 15: + if ("function" === typeof type) + return type.displayName || type.name || null; + if ("string" === typeof type) return type; + break; + case 29: + type = fiber._debugInfo; + if (null != type) + for (var i = type.length - 1; 0 <= i; i--) + if ("string" === typeof type[i].name) return type[i].name; + if (null !== fiber.return) + return getComponentNameFromFiber(fiber.return); + } + return null; + } + function createCursor(defaultValue) { + return { current: defaultValue }; + } + function pop(cursor, fiber) { + 0 > index$jscomp$0 + ? console.error("Unexpected pop.") + : (fiber !== fiberStack[index$jscomp$0] && + console.error("Unexpected Fiber popped."), + (cursor.current = valueStack[index$jscomp$0]), + (valueStack[index$jscomp$0] = null), + (fiberStack[index$jscomp$0] = null), + index$jscomp$0--); + } + function push(cursor, value, fiber) { + index$jscomp$0++; + valueStack[index$jscomp$0] = cursor.current; + fiberStack[index$jscomp$0] = fiber; + cursor.current = value; + } + function requiredContext(c) { + null === c && + console.error( + "Expected host context to exist. This error is likely caused by a bug in React. Please file an issue." + ); + return c; + } + function pushHostContainer(fiber, nextRootInstance) { + push(rootInstanceStackCursor, nextRootInstance, fiber); + push(contextFiberStackCursor, fiber, fiber); + push(contextStackCursor, null, fiber); + var nextRootContext = nextRootInstance.nodeType; + switch (nextRootContext) { + case 9: + case 11: + nextRootContext = 9 === nextRootContext ? "#document" : "#fragment"; + nextRootInstance = (nextRootInstance = + nextRootInstance.documentElement) + ? (nextRootInstance = nextRootInstance.namespaceURI) + ? getOwnHostContext(nextRootInstance) + : HostContextNamespaceNone + : HostContextNamespaceNone; + break; + default: + if ( + ((nextRootContext = nextRootInstance.tagName), + (nextRootInstance = nextRootInstance.namespaceURI)) + ) + (nextRootInstance = getOwnHostContext(nextRootInstance)), + (nextRootInstance = getChildHostContextProd( + nextRootInstance, + nextRootContext + )); + else + switch (nextRootContext) { + case "svg": + nextRootInstance = HostContextNamespaceSvg; + break; + case "math": + nextRootInstance = HostContextNamespaceMath; + break; + default: + nextRootInstance = HostContextNamespaceNone; + } + } + nextRootContext = nextRootContext.toLowerCase(); + nextRootContext = updatedAncestorInfoDev(null, nextRootContext); + nextRootContext = { + context: nextRootInstance, + ancestorInfo: nextRootContext + }; + pop(contextStackCursor, fiber); + push(contextStackCursor, nextRootContext, fiber); + } + function popHostContainer(fiber) { + pop(contextStackCursor, fiber); + pop(contextFiberStackCursor, fiber); + pop(rootInstanceStackCursor, fiber); + } + function getHostContext() { + return requiredContext(contextStackCursor.current); + } + function pushHostContext(fiber) { + null !== fiber.memoizedState && + push(hostTransitionProviderCursor, fiber, fiber); + var context = requiredContext(contextStackCursor.current); + var type = fiber.type; + var nextContext = getChildHostContextProd(context.context, type); + type = updatedAncestorInfoDev(context.ancestorInfo, type); + nextContext = { context: nextContext, ancestorInfo: type }; + context !== nextContext && + (push(contextFiberStackCursor, fiber, fiber), + push(contextStackCursor, nextContext, fiber)); + } + function popHostContext(fiber) { + contextFiberStackCursor.current === fiber && + (pop(contextStackCursor, fiber), pop(contextFiberStackCursor, fiber)); + hostTransitionProviderCursor.current === fiber && + (pop(hostTransitionProviderCursor, fiber), + (HostTransitionContext._currentValue = NotPendingTransition)); + } + function typeName(value) { + return ( + ("function" === typeof Symbol && + Symbol.toStringTag && + value[Symbol.toStringTag]) || + value.constructor.name || + "Object" + ); + } + function willCoercionThrow(value) { + try { + return testStringCoercion(value), !1; + } catch (e) { + return !0; + } + } + function testStringCoercion(value) { + return "" + value; + } + function checkAttributeStringCoercion(value, attributeName) { + if (willCoercionThrow(value)) + return ( + console.error( + "The provided `%s` attribute is an unsupported type %s. This value must be coerced to a string before using it here.", + attributeName, + typeName(value) + ), + testStringCoercion(value) + ); + } + function checkCSSPropertyStringCoercion(value, propName) { + if (willCoercionThrow(value)) + return ( + console.error( + "The provided `%s` CSS property is an unsupported type %s. This value must be coerced to a string before using it here.", + propName, + typeName(value) + ), + testStringCoercion(value) + ); + } + function checkFormFieldValueStringCoercion(value) { + if (willCoercionThrow(value)) + return ( + console.error( + "Form field values (value, checked, defaultValue, or defaultChecked props) must be strings, not %s. This value must be coerced to a string before using it here.", + typeName(value) + ), + testStringCoercion(value) + ); + } + function injectInternals(internals) { + if ("undefined" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) return !1; + var hook = __REACT_DEVTOOLS_GLOBAL_HOOK__; + if (hook.isDisabled) return !0; + if (!hook.supportsFiber) + return ( + console.error( + "The installed version of React DevTools is too old and will not work with the current version of React. Please update React DevTools. https://react.dev/link/react-devtools" + ), + !0 + ); + try { + (rendererID = hook.inject(internals)), (injectedHook = hook); + } catch (err) { + console.error("React instrumentation encountered an error: %s.", err); + } + return hook.checkDCE ? !0 : !1; + } + function setIsStrictModeForDevtools(newIsStrictMode) { + "function" === typeof log$1 && + unstable_setDisableYieldValue(newIsStrictMode); + if (injectedHook && "function" === typeof injectedHook.setStrictMode) + try { + injectedHook.setStrictMode(rendererID, newIsStrictMode); + } catch (err) { + hasLoggedError || + ((hasLoggedError = !0), + console.error( + "React instrumentation encountered an error: %s", + err + )); + } + } + function injectProfilingHooks(profilingHooks) { + injectedProfilingHooks = profilingHooks; + } + function markCommitStopped() { + null !== injectedProfilingHooks && + "function" === typeof injectedProfilingHooks.markCommitStopped && + injectedProfilingHooks.markCommitStopped(); + } + function markComponentRenderStarted(fiber) { + null !== injectedProfilingHooks && + "function" === + typeof injectedProfilingHooks.markComponentRenderStarted && + injectedProfilingHooks.markComponentRenderStarted(fiber); + } + function markComponentRenderStopped() { + null !== injectedProfilingHooks && + "function" === + typeof injectedProfilingHooks.markComponentRenderStopped && + injectedProfilingHooks.markComponentRenderStopped(); + } + function markRenderStarted(lanes) { + null !== injectedProfilingHooks && + "function" === typeof injectedProfilingHooks.markRenderStarted && + injectedProfilingHooks.markRenderStarted(lanes); + } + function markRenderStopped() { + null !== injectedProfilingHooks && + "function" === typeof injectedProfilingHooks.markRenderStopped && + injectedProfilingHooks.markRenderStopped(); + } + function markStateUpdateScheduled(fiber, lane) { + null !== injectedProfilingHooks && + "function" === typeof injectedProfilingHooks.markStateUpdateScheduled && + injectedProfilingHooks.markStateUpdateScheduled(fiber, lane); + } + function clz32Fallback(x) { + x >>>= 0; + return 0 === x ? 32 : (31 - ((log(x) / LN2) | 0)) | 0; + } + function getLabelForLane(lane) { + if (lane & 1) return "SyncHydrationLane"; + if (lane & 2) return "Sync"; + if (lane & 4) return "InputContinuousHydration"; + if (lane & 8) return "InputContinuous"; + if (lane & 16) return "DefaultHydration"; + if (lane & 32) return "Default"; + if (lane & 128) return "TransitionHydration"; + if (lane & 4194048) return "Transition"; + if (lane & 62914560) return "Retry"; + if (lane & 67108864) return "SelectiveHydration"; + if (lane & 134217728) return "IdleHydration"; + if (lane & 268435456) return "Idle"; + if (lane & 536870912) return "Offscreen"; + if (lane & 1073741824) return "Deferred"; + } + function getHighestPriorityLanes(lanes) { + var pendingSyncLanes = lanes & 42; + if (0 !== pendingSyncLanes) return pendingSyncLanes; + switch (lanes & -lanes) { + case 1: + return 1; + case 2: + return 2; + case 4: + return 4; + case 8: + return 8; + case 16: + return 16; + case 32: + return 32; + case 64: + return 64; + case 128: + return 128; + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + return lanes & 4194048; + case 4194304: + case 8388608: + case 16777216: + case 33554432: + return lanes & 62914560; + case 67108864: + return 67108864; + case 134217728: + return 134217728; + case 268435456: + return 268435456; + case 536870912: + return 536870912; + case 1073741824: + return 0; + default: + return ( + console.error( + "Should have found matching lanes. This is a bug in React." + ), + lanes + ); + } + } + function getNextLanes(root, wipLanes, rootHasPendingCommit) { + var pendingLanes = root.pendingLanes; + if (0 === pendingLanes) return 0; + var nextLanes = 0, + suspendedLanes = root.suspendedLanes, + pingedLanes = root.pingedLanes; + root = root.warmLanes; + var nonIdlePendingLanes = pendingLanes & 134217727; + 0 !== nonIdlePendingLanes + ? ((pendingLanes = nonIdlePendingLanes & ~suspendedLanes), + 0 !== pendingLanes + ? (nextLanes = getHighestPriorityLanes(pendingLanes)) + : ((pingedLanes &= nonIdlePendingLanes), + 0 !== pingedLanes + ? (nextLanes = getHighestPriorityLanes(pingedLanes)) + : rootHasPendingCommit || + ((rootHasPendingCommit = nonIdlePendingLanes & ~root), + 0 !== rootHasPendingCommit && + (nextLanes = + getHighestPriorityLanes(rootHasPendingCommit))))) + : ((nonIdlePendingLanes = pendingLanes & ~suspendedLanes), + 0 !== nonIdlePendingLanes + ? (nextLanes = getHighestPriorityLanes(nonIdlePendingLanes)) + : 0 !== pingedLanes + ? (nextLanes = getHighestPriorityLanes(pingedLanes)) + : rootHasPendingCommit || + ((rootHasPendingCommit = pendingLanes & ~root), + 0 !== rootHasPendingCommit && + (nextLanes = getHighestPriorityLanes(rootHasPendingCommit)))); + return 0 === nextLanes + ? 0 + : 0 !== wipLanes && + wipLanes !== nextLanes && + 0 === (wipLanes & suspendedLanes) && + ((suspendedLanes = nextLanes & -nextLanes), + (rootHasPendingCommit = wipLanes & -wipLanes), + suspendedLanes >= rootHasPendingCommit || + (32 === suspendedLanes && 0 !== (rootHasPendingCommit & 4194048))) + ? wipLanes + : nextLanes; + } + function checkIfRootIsPrerendering(root, renderLanes) { + return ( + 0 === + (root.pendingLanes & + ~(root.suspendedLanes & ~root.pingedLanes) & + renderLanes) + ); + } + function computeExpirationTime(lane, currentTime) { + switch (lane) { + case 1: + case 2: + case 4: + case 8: + case 64: + return currentTime + 250; + case 16: + case 32: + case 128: + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + return currentTime + 5e3; + case 4194304: + case 8388608: + case 16777216: + case 33554432: + return -1; + case 67108864: + case 134217728: + case 268435456: + case 536870912: + case 1073741824: + return -1; + default: + return ( + console.error( + "Should have found matching lanes. This is a bug in React." + ), + -1 + ); + } + } + function claimNextTransitionLane() { + var lane = nextTransitionLane; + nextTransitionLane <<= 1; + 0 === (nextTransitionLane & 4194048) && (nextTransitionLane = 256); + return lane; + } + function claimNextRetryLane() { + var lane = nextRetryLane; + nextRetryLane <<= 1; + 0 === (nextRetryLane & 62914560) && (nextRetryLane = 4194304); + return lane; + } + function createLaneMap(initial) { + for (var laneMap = [], i = 0; 31 > i; i++) laneMap.push(initial); + return laneMap; + } + function markRootUpdated$1(root, updateLane) { + root.pendingLanes |= updateLane; + 268435456 !== updateLane && + ((root.suspendedLanes = 0), + (root.pingedLanes = 0), + (root.warmLanes = 0)); + } + function markRootFinished( + root, + finishedLanes, + remainingLanes, + spawnedLane, + updatedLanes, + suspendedRetryLanes + ) { + var previouslyPendingLanes = root.pendingLanes; + root.pendingLanes = remainingLanes; + root.suspendedLanes = 0; + root.pingedLanes = 0; + root.warmLanes = 0; + root.expiredLanes &= remainingLanes; + root.entangledLanes &= remainingLanes; + root.errorRecoveryDisabledLanes &= remainingLanes; + root.shellSuspendCounter = 0; + var entanglements = root.entanglements, + expirationTimes = root.expirationTimes, + hiddenUpdates = root.hiddenUpdates; + for ( + remainingLanes = previouslyPendingLanes & ~remainingLanes; + 0 < remainingLanes; + + ) { + var index = 31 - clz32(remainingLanes), + lane = 1 << index; + entanglements[index] = 0; + expirationTimes[index] = -1; + var hiddenUpdatesForLane = hiddenUpdates[index]; + if (null !== hiddenUpdatesForLane) + for ( + hiddenUpdates[index] = null, index = 0; + index < hiddenUpdatesForLane.length; + index++ + ) { + var update = hiddenUpdatesForLane[index]; + null !== update && (update.lane &= -536870913); + } + remainingLanes &= ~lane; + } + 0 !== spawnedLane && markSpawnedDeferredLane(root, spawnedLane, 0); + 0 !== suspendedRetryLanes && + 0 === updatedLanes && + 0 !== root.tag && + (root.suspendedLanes |= + suspendedRetryLanes & ~(previouslyPendingLanes & ~finishedLanes)); + } + function markSpawnedDeferredLane(root, spawnedLane, entangledLanes) { + root.pendingLanes |= spawnedLane; + root.suspendedLanes &= ~spawnedLane; + var spawnedLaneIndex = 31 - clz32(spawnedLane); + root.entangledLanes |= spawnedLane; + root.entanglements[spawnedLaneIndex] = + root.entanglements[spawnedLaneIndex] | + 1073741824 | + (entangledLanes & 4194090); + } + function markRootEntangled(root, entangledLanes) { + var rootEntangledLanes = (root.entangledLanes |= entangledLanes); + for (root = root.entanglements; rootEntangledLanes; ) { + var index = 31 - clz32(rootEntangledLanes), + lane = 1 << index; + (lane & entangledLanes) | (root[index] & entangledLanes) && + (root[index] |= entangledLanes); + rootEntangledLanes &= ~lane; + } + } + function getBumpedLaneForHydrationByLane(lane) { + switch (lane) { + case 2: + lane = 1; + break; + case 8: + lane = 4; + break; + case 32: + lane = 16; + break; + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + case 4194304: + case 8388608: + case 16777216: + case 33554432: + lane = 128; + break; + case 268435456: + lane = 134217728; + break; + default: + lane = 0; + } + return lane; + } + function addFiberToLanesMap(root, fiber, lanes) { + if (isDevToolsPresent) + for (root = root.pendingUpdatersLaneMap; 0 < lanes; ) { + var index = 31 - clz32(lanes), + lane = 1 << index; + root[index].add(fiber); + lanes &= ~lane; + } + } + function movePendingFibersToMemoized(root, lanes) { + if (isDevToolsPresent) + for ( + var pendingUpdatersLaneMap = root.pendingUpdatersLaneMap, + memoizedUpdaters = root.memoizedUpdaters; + 0 < lanes; + + ) { + var index = 31 - clz32(lanes); + root = 1 << index; + index = pendingUpdatersLaneMap[index]; + 0 < index.size && + (index.forEach(function (fiber) { + var alternate = fiber.alternate; + (null !== alternate && memoizedUpdaters.has(alternate)) || + memoizedUpdaters.add(fiber); + }), + index.clear()); + lanes &= ~root; + } + } + function lanesToEventPriority(lanes) { + lanes &= -lanes; + return 0 !== DiscreteEventPriority && DiscreteEventPriority < lanes + ? 0 !== ContinuousEventPriority && ContinuousEventPriority < lanes + ? 0 !== (lanes & 134217727) + ? DefaultEventPriority + : IdleEventPriority + : ContinuousEventPriority + : DiscreteEventPriority; + } + function resolveUpdatePriority() { + var updatePriority = ReactDOMSharedInternals.p; + if (0 !== updatePriority) return updatePriority; + updatePriority = window.event; + return void 0 === updatePriority + ? DefaultEventPriority + : getEventPriority(updatePriority.type); + } + function runWithPriority(priority, fn) { + var previousPriority = ReactDOMSharedInternals.p; + try { + return (ReactDOMSharedInternals.p = priority), fn(); + } finally { + ReactDOMSharedInternals.p = previousPriority; + } + } + function detachDeletedInstance(node) { + delete node[internalInstanceKey]; + delete node[internalPropsKey]; + delete node[internalEventHandlersKey]; + delete node[internalEventHandlerListenersKey]; + delete node[internalEventHandlesSetKey]; + } + function getClosestInstanceFromNode(targetNode) { + var targetInst = targetNode[internalInstanceKey]; + if (targetInst) return targetInst; + for (var parentNode = targetNode.parentNode; parentNode; ) { + if ( + (targetInst = + parentNode[internalContainerInstanceKey] || + parentNode[internalInstanceKey]) + ) { + parentNode = targetInst.alternate; + if ( + null !== targetInst.child || + (null !== parentNode && null !== parentNode.child) + ) + for ( + targetNode = getParentSuspenseInstance(targetNode); + null !== targetNode; + + ) { + if ((parentNode = targetNode[internalInstanceKey])) + return parentNode; + targetNode = getParentSuspenseInstance(targetNode); + } + return targetInst; + } + targetNode = parentNode; + parentNode = targetNode.parentNode; + } + return null; + } + function getInstanceFromNode(node) { + if ( + (node = node[internalInstanceKey] || node[internalContainerInstanceKey]) + ) { + var tag = node.tag; + if ( + 5 === tag || + 6 === tag || + 13 === tag || + 26 === tag || + 27 === tag || + 3 === tag + ) + return node; + } + return null; + } + function getNodeFromInstance(inst) { + var tag = inst.tag; + if (5 === tag || 26 === tag || 27 === tag || 6 === tag) + return inst.stateNode; + throw Error("getNodeFromInstance: Invalid argument."); + } + function getResourcesFromRoot(root) { + var resources = root[internalRootNodeResourcesKey]; + resources || + (resources = root[internalRootNodeResourcesKey] = + { hoistableStyles: new Map(), hoistableScripts: new Map() }); + return resources; + } + function markNodeAsHoistable(node) { + node[internalHoistableMarker] = !0; + } + function registerTwoPhaseEvent(registrationName, dependencies) { + registerDirectEvent(registrationName, dependencies); + registerDirectEvent(registrationName + "Capture", dependencies); + } + function registerDirectEvent(registrationName, dependencies) { + registrationNameDependencies[registrationName] && + console.error( + "EventRegistry: More than one plugin attempted to publish the same registration name, `%s`.", + registrationName + ); + registrationNameDependencies[registrationName] = dependencies; + var lowerCasedName = registrationName.toLowerCase(); + possibleRegistrationNames[lowerCasedName] = registrationName; + "onDoubleClick" === registrationName && + (possibleRegistrationNames.ondblclick = registrationName); + for ( + registrationName = 0; + registrationName < dependencies.length; + registrationName++ + ) + allNativeEvents.add(dependencies[registrationName]); + } + function checkControlledValueProps(tagName, props) { + hasReadOnlyValue[props.type] || + props.onChange || + props.onInput || + props.readOnly || + props.disabled || + null == props.value || + ("select" === tagName + ? console.error( + "You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set `onChange`." + ) + : console.error( + "You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`." + )); + props.onChange || + props.readOnly || + props.disabled || + null == props.checked || + console.error( + "You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`." + ); + } + function isAttributeNameSafe(attributeName) { + if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) + return !0; + if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) + return !1; + if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) + return (validatedAttributeNameCache[attributeName] = !0); + illegalAttributeNameCache[attributeName] = !0; + console.error("Invalid attribute name: `%s`", attributeName); + return !1; + } + function getValueForAttributeOnCustomComponent(node, name, expected) { + if (isAttributeNameSafe(name)) { + if (!node.hasAttribute(name)) { + switch (typeof expected) { + case "symbol": + case "object": + return expected; + case "function": + return expected; + case "boolean": + if (!1 === expected) return expected; + } + return void 0 === expected ? void 0 : null; + } + node = node.getAttribute(name); + if ("" === node && !0 === expected) return !0; + checkAttributeStringCoercion(expected, name); + return node === "" + expected ? expected : node; + } + } + function setValueForAttribute(node, name, value) { + if (isAttributeNameSafe(name)) + if (null === value) node.removeAttribute(name); + else { + switch (typeof value) { + case "undefined": + case "function": + case "symbol": + node.removeAttribute(name); + return; + case "boolean": + var prefix = name.toLowerCase().slice(0, 5); + if ("data-" !== prefix && "aria-" !== prefix) { + node.removeAttribute(name); + return; + } + } + checkAttributeStringCoercion(value, name); + node.setAttribute(name, "" + value); + } + } + function setValueForKnownAttribute(node, name, value) { + if (null === value) node.removeAttribute(name); + else { + switch (typeof value) { + case "undefined": + case "function": + case "symbol": + case "boolean": + node.removeAttribute(name); + return; + } + checkAttributeStringCoercion(value, name); + node.setAttribute(name, "" + value); + } + } + function setValueForNamespacedAttribute(node, namespace, name, value) { + if (null === value) node.removeAttribute(name); + else { + switch (typeof value) { + case "undefined": + case "function": + case "symbol": + case "boolean": + node.removeAttribute(name); + return; + } + checkAttributeStringCoercion(value, name); + node.setAttributeNS(namespace, name, "" + value); + } + } + function disabledLog() {} + function disableLogs() { + if (0 === disabledDepth) { + prevLog = console.log; + prevInfo = console.info; + prevWarn = console.warn; + prevError = console.error; + prevGroup = console.group; + prevGroupCollapsed = console.groupCollapsed; + prevGroupEnd = console.groupEnd; + var props = { + configurable: !0, + enumerable: !0, + value: disabledLog, + writable: !0 + }; + Object.defineProperties(console, { + info: props, + log: props, + warn: props, + error: props, + group: props, + groupCollapsed: props, + groupEnd: props + }); + } + disabledDepth++; + } + function reenableLogs() { + disabledDepth--; + if (0 === disabledDepth) { + var props = { configurable: !0, enumerable: !0, writable: !0 }; + Object.defineProperties(console, { + log: assign({}, props, { value: prevLog }), + info: assign({}, props, { value: prevInfo }), + warn: assign({}, props, { value: prevWarn }), + error: assign({}, props, { value: prevError }), + group: assign({}, props, { value: prevGroup }), + groupCollapsed: assign({}, props, { value: prevGroupCollapsed }), + groupEnd: assign({}, props, { value: prevGroupEnd }) + }); + } + 0 > disabledDepth && + console.error( + "disabledDepth fell below zero. This is a bug in React. Please file an issue." + ); + } + function describeBuiltInComponentFrame(name) { + if (void 0 === prefix) + try { + throw Error(); + } catch (x) { + var match = x.stack.trim().match(/\n( *(at )?)/); + prefix = (match && match[1]) || ""; + suffix = + -1 < x.stack.indexOf("\n at") + ? " ()" + : -1 < x.stack.indexOf("@") + ? "@unknown:0:0" + : ""; + } + return "\n" + prefix + name + suffix; + } + function describeNativeComponentFrame(fn, construct) { + if (!fn || reentry) return ""; + var frame = componentFrameCache.get(fn); + if (void 0 !== frame) return frame; + reentry = !0; + frame = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + var previousDispatcher = null; + previousDispatcher = ReactSharedInternals.H; + ReactSharedInternals.H = null; + disableLogs(); + try { + var RunInRootFrame = { + DetermineComponentFrameRoot: function () { + try { + if (construct) { + var Fake = function () { + throw Error(); + }; + Object.defineProperty(Fake.prototype, "props", { + set: function () { + throw Error(); + } + }); + if ("object" === typeof Reflect && Reflect.construct) { + try { + Reflect.construct(Fake, []); + } catch (x) { + var control = x; + } + Reflect.construct(fn, [], Fake); + } else { + try { + Fake.call(); + } catch (x$0) { + control = x$0; + } + fn.call(Fake.prototype); + } + } else { + try { + throw Error(); + } catch (x$1) { + control = x$1; + } + (Fake = fn()) && + "function" === typeof Fake.catch && + Fake.catch(function () {}); + } + } catch (sample) { + if (sample && control && "string" === typeof sample.stack) + return [sample.stack, control.stack]; + } + return [null, null]; + } + }; + RunInRootFrame.DetermineComponentFrameRoot.displayName = + "DetermineComponentFrameRoot"; + var namePropDescriptor = Object.getOwnPropertyDescriptor( + RunInRootFrame.DetermineComponentFrameRoot, + "name" + ); + namePropDescriptor && + namePropDescriptor.configurable && + Object.defineProperty( + RunInRootFrame.DetermineComponentFrameRoot, + "name", + { value: "DetermineComponentFrameRoot" } + ); + var _RunInRootFrame$Deter = + RunInRootFrame.DetermineComponentFrameRoot(), + sampleStack = _RunInRootFrame$Deter[0], + controlStack = _RunInRootFrame$Deter[1]; + if (sampleStack && controlStack) { + var sampleLines = sampleStack.split("\n"), + controlLines = controlStack.split("\n"); + for ( + _RunInRootFrame$Deter = namePropDescriptor = 0; + namePropDescriptor < sampleLines.length && + !sampleLines[namePropDescriptor].includes( + "DetermineComponentFrameRoot" + ); + + ) + namePropDescriptor++; + for ( + ; + _RunInRootFrame$Deter < controlLines.length && + !controlLines[_RunInRootFrame$Deter].includes( + "DetermineComponentFrameRoot" + ); + + ) + _RunInRootFrame$Deter++; + if ( + namePropDescriptor === sampleLines.length || + _RunInRootFrame$Deter === controlLines.length + ) + for ( + namePropDescriptor = sampleLines.length - 1, + _RunInRootFrame$Deter = controlLines.length - 1; + 1 <= namePropDescriptor && + 0 <= _RunInRootFrame$Deter && + sampleLines[namePropDescriptor] !== + controlLines[_RunInRootFrame$Deter]; + + ) + _RunInRootFrame$Deter--; + for ( + ; + 1 <= namePropDescriptor && 0 <= _RunInRootFrame$Deter; + namePropDescriptor--, _RunInRootFrame$Deter-- + ) + if ( + sampleLines[namePropDescriptor] !== + controlLines[_RunInRootFrame$Deter] + ) { + if (1 !== namePropDescriptor || 1 !== _RunInRootFrame$Deter) { + do + if ( + (namePropDescriptor--, + _RunInRootFrame$Deter--, + 0 > _RunInRootFrame$Deter || + sampleLines[namePropDescriptor] !== + controlLines[_RunInRootFrame$Deter]) + ) { + var _frame = + "\n" + + sampleLines[namePropDescriptor].replace( + " at new ", + " at " + ); + fn.displayName && + _frame.includes("") && + (_frame = _frame.replace("", fn.displayName)); + "function" === typeof fn && + componentFrameCache.set(fn, _frame); + return _frame; + } + while (1 <= namePropDescriptor && 0 <= _RunInRootFrame$Deter); + } + break; + } + } + } finally { + (reentry = !1), + (ReactSharedInternals.H = previousDispatcher), + reenableLogs(), + (Error.prepareStackTrace = frame); + } + sampleLines = (sampleLines = fn ? fn.displayName || fn.name : "") + ? describeBuiltInComponentFrame(sampleLines) + : ""; + "function" === typeof fn && componentFrameCache.set(fn, sampleLines); + return sampleLines; + } + function formatOwnerStack(error) { + var prevPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + error = error.stack; + Error.prepareStackTrace = prevPrepareStackTrace; + error.startsWith("Error: react-stack-top-frame\n") && + (error = error.slice(29)); + prevPrepareStackTrace = error.indexOf("\n"); + -1 !== prevPrepareStackTrace && + (error = error.slice(prevPrepareStackTrace + 1)); + prevPrepareStackTrace = error.indexOf("react-stack-bottom-frame"); + -1 !== prevPrepareStackTrace && + (prevPrepareStackTrace = error.lastIndexOf( + "\n", + prevPrepareStackTrace + )); + if (-1 !== prevPrepareStackTrace) + error = error.slice(0, prevPrepareStackTrace); + else return ""; + return error; + } + function describeFiber(fiber) { + switch (fiber.tag) { + case 26: + case 27: + case 5: + return describeBuiltInComponentFrame(fiber.type); + case 16: + return describeBuiltInComponentFrame("Lazy"); + case 13: + return describeBuiltInComponentFrame("Suspense"); + case 19: + return describeBuiltInComponentFrame("SuspenseList"); + case 0: + case 15: + return describeNativeComponentFrame(fiber.type, !1); + case 11: + return describeNativeComponentFrame(fiber.type.render, !1); + case 1: + return describeNativeComponentFrame(fiber.type, !0); + case 31: + return describeBuiltInComponentFrame("Activity"); + default: + return ""; + } + } + function getStackByFiberInDevAndProd(workInProgress) { + try { + var info = ""; + do { + info += describeFiber(workInProgress); + var debugInfo = workInProgress._debugInfo; + if (debugInfo) + for (var i = debugInfo.length - 1; 0 <= i; i--) { + var entry = debugInfo[i]; + if ("string" === typeof entry.name) { + var JSCompiler_temp_const = info, + env = entry.env; + var JSCompiler_inline_result = describeBuiltInComponentFrame( + entry.name + (env ? " [" + env + "]" : "") + ); + info = JSCompiler_temp_const + JSCompiler_inline_result; + } + } + workInProgress = workInProgress.return; + } while (workInProgress); + return info; + } catch (x) { + return "\nError generating stack: " + x.message + "\n" + x.stack; + } + } + function describeFunctionComponentFrameWithoutLineNumber(fn) { + return (fn = fn ? fn.displayName || fn.name : "") + ? describeBuiltInComponentFrame(fn) + : ""; + } + function getCurrentFiberOwnerNameInDevOrNull() { + if (null === current) return null; + var owner = current._debugOwner; + return null != owner ? getComponentNameFromOwner(owner) : null; + } + function getCurrentFiberStackInDev() { + if (null === current) return ""; + var workInProgress = current; + try { + var info = ""; + 6 === workInProgress.tag && (workInProgress = workInProgress.return); + switch (workInProgress.tag) { + case 26: + case 27: + case 5: + info += describeBuiltInComponentFrame(workInProgress.type); + break; + case 13: + info += describeBuiltInComponentFrame("Suspense"); + break; + case 19: + info += describeBuiltInComponentFrame("SuspenseList"); + break; + case 31: + info += describeBuiltInComponentFrame("Activity"); + break; + case 30: + case 0: + case 15: + case 1: + workInProgress._debugOwner || + "" !== info || + (info += describeFunctionComponentFrameWithoutLineNumber( + workInProgress.type + )); + break; + case 11: + workInProgress._debugOwner || + "" !== info || + (info += describeFunctionComponentFrameWithoutLineNumber( + workInProgress.type.render + )); + } + for (; workInProgress; ) + if ("number" === typeof workInProgress.tag) { + var fiber = workInProgress; + workInProgress = fiber._debugOwner; + var debugStack = fiber._debugStack; + workInProgress && + debugStack && + ("string" !== typeof debugStack && + (fiber._debugStack = debugStack = formatOwnerStack(debugStack)), + "" !== debugStack && (info += "\n" + debugStack)); + } else if (null != workInProgress.debugStack) { + var ownerStack = workInProgress.debugStack; + (workInProgress = workInProgress.owner) && + ownerStack && + (info += "\n" + formatOwnerStack(ownerStack)); + } else break; + var JSCompiler_inline_result = info; + } catch (x) { + JSCompiler_inline_result = + "\nError generating stack: " + x.message + "\n" + x.stack; + } + return JSCompiler_inline_result; + } + function runWithFiberInDEV(fiber, callback, arg0, arg1, arg2, arg3, arg4) { + var previousFiber = current; + setCurrentFiber(fiber); + try { + return null !== fiber && fiber._debugTask + ? fiber._debugTask.run( + callback.bind(null, arg0, arg1, arg2, arg3, arg4) + ) + : callback(arg0, arg1, arg2, arg3, arg4); + } finally { + setCurrentFiber(previousFiber); + } + throw Error( + "runWithFiberInDEV should never be called in production. This is a bug in React." + ); + } + function setCurrentFiber(fiber) { + ReactSharedInternals.getCurrentStack = + null === fiber ? null : getCurrentFiberStackInDev; + isRendering = !1; + current = fiber; + } + function getToStringValue(value) { + switch (typeof value) { + case "bigint": + case "boolean": + case "number": + case "string": + case "undefined": + return value; + case "object": + return checkFormFieldValueStringCoercion(value), value; + default: + return ""; + } + } + function isCheckable(elem) { + var type = elem.type; + return ( + (elem = elem.nodeName) && + "input" === elem.toLowerCase() && + ("checkbox" === type || "radio" === type) + ); + } + function trackValueOnNode(node) { + var valueField = isCheckable(node) ? "checked" : "value", + descriptor = Object.getOwnPropertyDescriptor( + node.constructor.prototype, + valueField + ); + checkFormFieldValueStringCoercion(node[valueField]); + var currentValue = "" + node[valueField]; + if ( + !node.hasOwnProperty(valueField) && + "undefined" !== typeof descriptor && + "function" === typeof descriptor.get && + "function" === typeof descriptor.set + ) { + var get = descriptor.get, + set = descriptor.set; + Object.defineProperty(node, valueField, { + configurable: !0, + get: function () { + return get.call(this); + }, + set: function (value) { + checkFormFieldValueStringCoercion(value); + currentValue = "" + value; + set.call(this, value); + } + }); + Object.defineProperty(node, valueField, { + enumerable: descriptor.enumerable + }); + return { + getValue: function () { + return currentValue; + }, + setValue: function (value) { + checkFormFieldValueStringCoercion(value); + currentValue = "" + value; + }, + stopTracking: function () { + node._valueTracker = null; + delete node[valueField]; + } + }; + } + } + function track(node) { + node._valueTracker || (node._valueTracker = trackValueOnNode(node)); + } + function updateValueIfChanged(node) { + if (!node) return !1; + var tracker = node._valueTracker; + if (!tracker) return !0; + var lastValue = tracker.getValue(); + var value = ""; + node && + (value = isCheckable(node) + ? node.checked + ? "true" + : "false" + : node.value); + node = value; + return node !== lastValue ? (tracker.setValue(node), !0) : !1; + } + function getActiveElement(doc) { + doc = doc || ("undefined" !== typeof document ? document : void 0); + if ("undefined" === typeof doc) return null; + try { + return doc.activeElement || doc.body; + } catch (e) { + return doc.body; + } + } + function escapeSelectorAttributeValueInsideDoubleQuotes(value) { + return value.replace( + escapeSelectorAttributeValueInsideDoubleQuotesRegex, + function (ch) { + return "\\" + ch.charCodeAt(0).toString(16) + " "; + } + ); + } + function validateInputProps(element, props) { + void 0 === props.checked || + void 0 === props.defaultChecked || + didWarnCheckedDefaultChecked || + (console.error( + "%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components", + getCurrentFiberOwnerNameInDevOrNull() || "A component", + props.type + ), + (didWarnCheckedDefaultChecked = !0)); + void 0 === props.value || + void 0 === props.defaultValue || + didWarnValueDefaultValue$1 || + (console.error( + "%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components", + getCurrentFiberOwnerNameInDevOrNull() || "A component", + props.type + ), + (didWarnValueDefaultValue$1 = !0)); + } + function updateInput( + element, + value, + defaultValue, + lastDefaultValue, + checked, + defaultChecked, + type, + name + ) { + element.name = ""; + null != type && + "function" !== typeof type && + "symbol" !== typeof type && + "boolean" !== typeof type + ? (checkAttributeStringCoercion(type, "type"), (element.type = type)) + : element.removeAttribute("type"); + if (null != value) + if ("number" === type) { + if ((0 === value && "" === element.value) || element.value != value) + element.value = "" + getToStringValue(value); + } else + element.value !== "" + getToStringValue(value) && + (element.value = "" + getToStringValue(value)); + else + ("submit" !== type && "reset" !== type) || + element.removeAttribute("value"); + null != value + ? setDefaultValue(element, type, getToStringValue(value)) + : null != defaultValue + ? setDefaultValue(element, type, getToStringValue(defaultValue)) + : null != lastDefaultValue && element.removeAttribute("value"); + null == checked && + null != defaultChecked && + (element.defaultChecked = !!defaultChecked); + null != checked && + (element.checked = + checked && + "function" !== typeof checked && + "symbol" !== typeof checked); + null != name && + "function" !== typeof name && + "symbol" !== typeof name && + "boolean" !== typeof name + ? (checkAttributeStringCoercion(name, "name"), + (element.name = "" + getToStringValue(name))) + : element.removeAttribute("name"); + } + function initInput( + element, + value, + defaultValue, + checked, + defaultChecked, + type, + name, + isHydrating + ) { + null != type && + "function" !== typeof type && + "symbol" !== typeof type && + "boolean" !== typeof type && + (checkAttributeStringCoercion(type, "type"), (element.type = type)); + if (null != value || null != defaultValue) { + if ( + !( + ("submit" !== type && "reset" !== type) || + (void 0 !== value && null !== value) + ) + ) + return; + defaultValue = + null != defaultValue ? "" + getToStringValue(defaultValue) : ""; + value = null != value ? "" + getToStringValue(value) : defaultValue; + isHydrating || value === element.value || (element.value = value); + element.defaultValue = value; + } + checked = null != checked ? checked : defaultChecked; + checked = + "function" !== typeof checked && + "symbol" !== typeof checked && + !!checked; + element.checked = isHydrating ? element.checked : !!checked; + element.defaultChecked = !!checked; + null != name && + "function" !== typeof name && + "symbol" !== typeof name && + "boolean" !== typeof name && + (checkAttributeStringCoercion(name, "name"), (element.name = name)); + } + function setDefaultValue(node, type, value) { + ("number" === type && getActiveElement(node.ownerDocument) === node) || + node.defaultValue === "" + value || + (node.defaultValue = "" + value); + } + function validateOptionProps(element, props) { + null == props.value && + ("object" === typeof props.children && null !== props.children + ? React.Children.forEach(props.children, function (child) { + null == child || + "string" === typeof child || + "number" === typeof child || + "bigint" === typeof child || + didWarnInvalidChild || + ((didWarnInvalidChild = !0), + console.error( + "Cannot infer the option value of complex children. Pass a `value` prop or use a plain string as children to