代码提交
This commit is contained in:
@@ -153,22 +153,28 @@ export default function MaterialsPage({ params }: { params: Promise<{ id: string
|
||||
href={first.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center bg-white rounded p-2 hover:bg-gray-50 transition group"
|
||||
className=" items-center bg-white rounded p-2 hover:bg-gray-50 transition group"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
{first.image && (
|
||||
<div className="mb-3">
|
||||
<p className="text-gray-700 whitespace-pre-line">{material.content}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center" style={{ border: '1px solid #ededed' }}>
|
||||
<div className="flex-shrink-0 w-14 h-14 rounded overflow-hidden mr-3 bg-gray-100">
|
||||
<Image
|
||||
src={first.image}
|
||||
src={first.image ?? 'https://api.dicebear.com/7.x/avataaars/svg?seed=123'}
|
||||
alt="封面图"
|
||||
width={56}
|
||||
height={56}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base font-medium truncate group-hover:text-blue-600">{first.desc}</div>
|
||||
<div className="text-base font-medium truncate">{first.desc ?? '这是一条链接'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
@@ -179,6 +185,7 @@ export default function MaterialsPage({ params }: { params: Promise<{ id: string
|
||||
const videoUrl = typeof first === "string" ? first : (first.url || "");
|
||||
return videoUrl ? (
|
||||
<div className="mb-3">
|
||||
<p className="text-gray-700 whitespace-pre-line">{material.content}</p>
|
||||
<video src={videoUrl} controls className="rounded w-full max-w-md" />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -2,13 +2,53 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DeviceSelector } from "@/app/components/common/DeviceSelector"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { DeviceSelectionDialog } from "@/app/components/device-selection-dialog"
|
||||
import { ChevronLeft, Trash2 } from "lucide-react"
|
||||
|
||||
interface WechatAccount {
|
||||
avatar: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
}
|
||||
interface Device {
|
||||
id: string
|
||||
imei: string
|
||||
remark?: string
|
||||
wechatAccounts: WechatAccount[]
|
||||
online: boolean
|
||||
friendStatus: "正常" | "异常"
|
||||
}
|
||||
|
||||
// mock 设备数据
|
||||
const mockDevices: Device[] = [
|
||||
{
|
||||
id: "aa6d4c2f7b1fe24d04d34f4f409883e6",
|
||||
imei: "aa6d4c2f7b1fe24d04d34f4f409883e6",
|
||||
remark: "游戏2 19445",
|
||||
wechatAccounts: [
|
||||
{
|
||||
avatar: "https://img2.baidu.com/it/u=123456789,123456789&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
|
||||
nickname: "老钟爹-解放双手,释放时间",
|
||||
wechatId: "wxid_480es52qsj2812"
|
||||
},
|
||||
{
|
||||
avatar: "https://img2.baidu.com/it/u=123456789,123456789&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
|
||||
nickname: "",
|
||||
wechatId: "w28533368 15375804003"
|
||||
}
|
||||
],
|
||||
online: true,
|
||||
friendStatus: "正常"
|
||||
}
|
||||
]
|
||||
|
||||
export default function ScenarioDevicesPage({ params }: { params: { channel: string } }) {
|
||||
const router = useRouter()
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([])
|
||||
const [isDeviceSelectorOpen, setIsDeviceSelectorOpen] = useState(false)
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>(mockDevices.map(d=>d.id))
|
||||
const [selectedDevices, setSelectedDevices] = useState<Device[]>(mockDevices)
|
||||
|
||||
// 获取渠道中文名称
|
||||
const getChannelName = (channel: string) => {
|
||||
@@ -30,14 +70,19 @@ export default function ScenarioDevicesPage({ params }: { params: { channel: str
|
||||
|
||||
const channelName = getChannelName(params.channel)
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 这里应该是实际的API调用来保存选中的设备
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
router.back()
|
||||
} catch (error) {
|
||||
console.error("保存失败:", error)
|
||||
// 设备选择回填
|
||||
const handleDeviceSelect = (deviceIds: string[]) => {
|
||||
setSelectedDeviceIds(deviceIds)
|
||||
// 这里用mockDevices过滤,实际应接口获取
|
||||
setSelectedDevices(mockDevices.filter(d => deviceIds.includes(d.id)))
|
||||
setIsDeviceSelectorOpen(false)
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
const handleDelete = (id: string) => {
|
||||
const newIds = selectedDeviceIds.filter(did => did !== id)
|
||||
setSelectedDeviceIds(newIds)
|
||||
setSelectedDevices(selectedDevices.filter(d => d.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -54,20 +99,115 @@ export default function ScenarioDevicesPage({ params }: { params: { channel: str
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<DeviceSelector
|
||||
title={`${channelName}设备选择`}
|
||||
selectedDevices={selectedDevices}
|
||||
onDevicesChange={setSelectedDevices}
|
||||
multiple={true}
|
||||
maxSelection={5}
|
||||
className="mb-4"
|
||||
<div className="flex justify-end mb-2">
|
||||
<Button onClick={() => setIsDeviceSelectorOpen(true)}>+ 选择设备</Button>
|
||||
<DeviceSelectionDialog
|
||||
open={isDeviceSelectorOpen}
|
||||
onOpenChange={setIsDeviceSelectorOpen}
|
||||
selectedDevices={selectedDeviceIds}
|
||||
onSelect={handleDeviceSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* PC端表格 */}
|
||||
<div className="overflow-x-auto bg-white rounded shadow hidden md:block">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-4 py-2 text-left">设备IMEI/备注/手机/设备ID</th>
|
||||
<th className="px-4 py-2 text-left">客服微信号</th>
|
||||
<th className="px-4 py-2">在线</th>
|
||||
<th className="px-4 py-2">加友状态</th>
|
||||
<th className="px-4 py-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedDevices.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-8 text-gray-400">暂无已选设备</td>
|
||||
</tr>
|
||||
) : (
|
||||
selectedDevices.map(device => (
|
||||
<tr key={device.id} className="border-t">
|
||||
<td className="px-4 py-2 whitespace-pre-line">
|
||||
{device.imei}
|
||||
{device.remark ? <div className="text-xs text-gray-500">{device.remark}</div> : null}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{device.wechatAccounts.length === 0 ? (
|
||||
<span className="text-gray-400">-</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{device.wechatAccounts.map((wx, idx) => (
|
||||
<div key={wx.wechatId+idx} className="flex items-center space-x-2">
|
||||
{wx.avatar && <img src={wx.avatar} alt="avatar" className="w-7 h-7 rounded object-cover" />}
|
||||
<span>{wx.nickname}</span>
|
||||
<span className="text-xs text-gray-500">{wx.wechatId}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<Badge variant={device.online ? "success" : "secondary"}>{device.online ? "是" : "否"}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<Badge variant={device.friendStatus === "正常" ? "success" : "destructive"}>{device.friendStatus}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<Button variant="destructive" size="icon" onClick={() => handleDelete(device.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 移动端卡片式渲染 */}
|
||||
<div className="space-y-3 md:hidden">
|
||||
{selectedDevices.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400 bg-white rounded">暂无已选设备</div>
|
||||
) : (
|
||||
selectedDevices.map(device => (
|
||||
<div key={device.id} className="bg-white rounded shadow p-3">
|
||||
<div className="font-medium break-all">{device.imei}</div>
|
||||
{device.remark && <div className="text-xs text-gray-500 mb-1">{device.remark}</div>}
|
||||
<div className="mb-1">
|
||||
<span className="text-gray-500 text-xs">客服微信号:</span>
|
||||
{device.wechatAccounts.length === 0 ? (
|
||||
<span className="text-gray-400">-</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{device.wechatAccounts.map((wx, idx) => (
|
||||
<div key={wx.wechatId+idx} className="flex items-center space-x-2">
|
||||
{wx.avatar && <img src={wx.avatar} alt="avatar" className="w-6 h-6 rounded object-cover" />}
|
||||
<span className="truncate">{wx.nickname}</span>
|
||||
<span className="text-xs text-gray-500">{wx.wechatId}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Badge variant={device.online ? "success" : "secondary"}>{device.online ? "在线" : "离线"}</Badge>
|
||||
<Badge variant={device.friendStatus === "正常" ? "success" : "destructive"}>{device.friendStatus}</Badge>
|
||||
<Button variant="destructive" size="icon" onClick={() => handleDelete(device.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white p-4 border-t flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => router.back()}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={selectedDevices.length === 0}>
|
||||
<Button onClick={() => {}} disabled={selectedDevices.length === 0}>
|
||||
保存 ({selectedDevices.length})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: any
|
||||
@@ -48,7 +49,7 @@ interface PosterSectionProps {
|
||||
onUpload: () => void
|
||||
onSelect: (material: Material) => void
|
||||
uploading: boolean
|
||||
fileInputRef: React.RefObject<HTMLInputElement>
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>
|
||||
onFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onPreview: (url: string) => void
|
||||
onRemove: (id: string) => void
|
||||
@@ -58,7 +59,7 @@ interface OrderSectionProps {
|
||||
materials: Material[]
|
||||
onUpload: () => void
|
||||
uploading: boolean
|
||||
fileInputRef: React.RefObject<HTMLInputElement>
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>
|
||||
onFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ interface DouyinSectionProps {
|
||||
materials: Material[]
|
||||
onUpload: () => void
|
||||
uploading: boolean
|
||||
fileInputRef: React.RefObject<HTMLInputElement>
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>
|
||||
onFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
@@ -330,6 +331,7 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false)
|
||||
const [importedTags, setImportedTags] = useState<
|
||||
Array<{
|
||||
name: string
|
||||
phone: string
|
||||
wechat: string
|
||||
source?: string
|
||||
@@ -352,25 +354,29 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
const [selectedPhoneTags, setSelectedPhoneTags] = useState<string[]>(formData.phoneTags || [])
|
||||
const [phoneCallType, setPhoneCallType] = useState(formData.phoneCallType || "both")
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [uploadingPoster, setUploadingPoster] = useState(false)
|
||||
|
||||
// 新增不同场景的materials和上传逻辑
|
||||
const [orderMaterials, setOrderMaterials] = useState<any[]>([])
|
||||
const [douyinMaterials, setDouyinMaterials] = useState<any[]>([])
|
||||
const orderFileInputRef = useRef<HTMLInputElement>(null)
|
||||
const douyinFileInputRef = useRef<HTMLInputElement>(null)
|
||||
const orderFileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const douyinFileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [uploadingOrder, setUploadingOrder] = useState(false)
|
||||
const [uploadingDouyin, setUploadingDouyin] = useState(false)
|
||||
|
||||
// 新增小程序和链接封面上传相关state和ref
|
||||
const [miniAppCover, setMiniAppCover] = useState(formData.miniAppCover || "")
|
||||
const [uploadingMiniAppCover, setUploadingMiniAppCover] = useState(false)
|
||||
const miniAppFileInputRef = useRef<HTMLInputElement>(null)
|
||||
const miniAppFileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [linkCover, setLinkCover] = useState(formData.linkCover || "")
|
||||
const [uploadingLinkCover, setUploadingLinkCover] = useState(false)
|
||||
const linkFileInputRef = useRef<HTMLInputElement>(null)
|
||||
const linkFileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [uploadingOrderTable, setUploadingOrderTable] = useState(false)
|
||||
const [uploadedOrderTableFile, setUploadedOrderTableFile] = useState<string>(formData.orderTableFileName || "")
|
||||
const orderTableFileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const type = searchParams.get("type")
|
||||
@@ -532,8 +538,9 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
const content = e.target?.result as string
|
||||
const rows = content.split("\n").filter((row) => row.trim())
|
||||
const tags = rows.slice(1).map((row) => {
|
||||
const [phone, wechat, source, orderAmount, orderDate] = row.split(",")
|
||||
const [name, phone, wechat, source, orderAmount, orderDate] = row.split(",")
|
||||
return {
|
||||
name: name.trim(),
|
||||
phone: phone.trim(),
|
||||
wechat: wechat.trim(),
|
||||
source: source?.trim(),
|
||||
@@ -552,7 +559,7 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
}
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
const template = "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"
|
||||
const template = "姓名,电话号码,微信号,来源,订单金额,下单日期\n张三,13800138000,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")
|
||||
@@ -607,6 +614,7 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
const newPoster = {
|
||||
id: `custom_${Date.now()}`,
|
||||
name: result.data.name || '自定义海报',
|
||||
type: 'poster',
|
||||
preview: result.data.url,
|
||||
}
|
||||
setMaterials(prev => [newPoster, ...prev])
|
||||
@@ -748,7 +756,43 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadOrderTable = () => {
|
||||
orderTableFileInputRef.current?.click()
|
||||
}
|
||||
|
||||
const handleOrderTableFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
setUploadingOrderTable(true)
|
||||
const formDataObj = new FormData()
|
||||
formDataObj.append('file', file)
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const headers: HeadersInit = {}
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formDataObj,
|
||||
})
|
||||
const result = await response.json()
|
||||
if (result.code === 200 && result.data?.url) {
|
||||
setUploadedOrderTableFile(file.name)
|
||||
onChange({ ...formData, orderTableFile: result.data.url, orderTableFileName: file.name })
|
||||
toast({ title: '上传成功', description: '订单表格文件已上传' })
|
||||
} else {
|
||||
toast({ title: '上传失败', description: result.msg || '请重试', variant: 'destructive' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast({ title: '上传失败', description: e?.message || '请重试', variant: 'destructive' })
|
||||
} finally {
|
||||
setUploadingOrderTable(false)
|
||||
if (orderTableFileInputRef.current) orderTableFileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const renderSceneExtra = () => {
|
||||
console.log('currentScenario:', currentScenario?.name);
|
||||
switch (currentScenario?.name) {
|
||||
case "海报获客":
|
||||
return (
|
||||
@@ -844,6 +888,44 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
</div>
|
||||
)
|
||||
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const handleSave = async () => {
|
||||
if (saving) return // 防止重复点击
|
||||
setSaving(true)
|
||||
try {
|
||||
// ...原有代码...
|
||||
const submitData = {
|
||||
...formData,
|
||||
device: formData.selectedDevices || formData.device,
|
||||
posters: formData.materials || formData.posters,
|
||||
};
|
||||
const { selectedDevices, materials, ...finalData } = submitData;
|
||||
const res = await api.post<ApiResponse>("/v1/plan/create", finalData);
|
||||
if (res.code === 200) {
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: "获客计划已创建",
|
||||
})
|
||||
router.push(`/scenarios/${formData.sceneId}`)
|
||||
} else {
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: res.msg || "创建计划失败,请重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: error?.message || "创建计划失败,请重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card className="p-6">
|
||||
@@ -1141,58 +1223,36 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
</div>
|
||||
))}
|
||||
|
||||
{String(currentScenario?.id) === "order" && (
|
||||
{currentScenario?.name === "订单获客" && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label>订单导入</Label>
|
||||
<div className="flex gap-2">
|
||||
<Label>订单表格上传</Label>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button variant="outline" onClick={handleDownloadTemplate}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
下载模板
|
||||
</Button>
|
||||
<Button onClick={() => setIsImportDialogOpen(true)}>
|
||||
<Button onClick={handleUploadOrderTable} disabled={uploadingOrderTable} variant="outline">
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
导入订单
|
||||
上传订单表格
|
||||
<input
|
||||
type="file"
|
||||
ref={orderTableFileInputRef}
|
||||
onChange={handleOrderTableFileChange}
|
||||
className="hidden"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importedTags.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium mb-2">已导入 {importedTags.length} 条数据</h4>
|
||||
<div className="max-h-[300px] overflow-auto border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>电话号码</TableHead>
|
||||
<TableHead>微信号</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>订单金额</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importedTags.slice(0, 5).map((tag, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{tag.phone}</TableCell>
|
||||
<TableCell>{tag.wechat}</TableCell>
|
||||
<TableCell>{tag.source}</TableCell>
|
||||
<TableCell>{tag.orderAmount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{importedTags.length > 5 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-gray-500">
|
||||
还有 {importedTags.length - 5} 条数据未显示
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{uploadedOrderTableFile && (
|
||||
<div className="mt-2 text-xs text-green-600 text-left">已上传:{uploadedOrderTableFile}</div>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="text-xs text-gray-500 mt-1">支持 CSV、Excel 格式,上传后将文件保存到服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{String(formData.scenario) === "weixinqun" && (
|
||||
<>
|
||||
<div>
|
||||
@@ -1284,8 +1344,8 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button className="w-full h-12 text-base" onClick={onNext}>
|
||||
下一步
|
||||
<Button className="w-full h-12 text-base" onClick={onNext} disabled={saving}>
|
||||
{saving ? <span className="flex items-center justify-center"><svg className="animate-spin h-5 w-5 mr-2 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path></svg>提交中...</span> : "下一步"}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1405,56 +1465,6 @@ export function BasicSettings({ formData, onChange, onNext, scenarios, loadingSc
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isImportDialogOpen} onOpenChange={setIsImportDialogOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入订单标签</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input type="file" accept=".csv" onChange={handleFileImport} className="flex-1" />
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>电话号码</TableHead>
|
||||
<TableHead>微信号</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>订单金额</TableHead>
|
||||
<TableHead>下单日期</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importedTags.map((tag, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{tag.phone}</TableCell>
|
||||
<TableCell>{tag.wechat}</TableCell>
|
||||
<TableCell>{tag.source}</TableCell>
|
||||
<TableCell>{tag.orderAmount}</TableCell>
|
||||
<TableCell>{tag.orderDate}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsImportDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange({ ...formData, importedTags })
|
||||
setIsImportDialogOpen(false)
|
||||
}}
|
||||
>
|
||||
确认导入
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,16 +26,7 @@ interface TaskDetail {
|
||||
startTime: string
|
||||
endTime: string
|
||||
enabled: boolean
|
||||
devices: {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
}[]
|
||||
contentLibraries: {
|
||||
id: string
|
||||
name: string
|
||||
count: number
|
||||
}[]
|
||||
config: any
|
||||
lastSyncTime: string
|
||||
createTime: string
|
||||
creator: string
|
||||
@@ -79,7 +70,7 @@ export default function MomentsSyncDetailPage({ params }: { params: { id: string
|
||||
const fetchTaskDetail = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/moments-records?workbenchId=${params.id}`)
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/detail?id=${params.id}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
setTaskDetail(response.data)
|
||||
|
||||
@@ -260,33 +251,6 @@ export default function MomentsSyncDetailPage({ params }: { params: { id: string
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">任务详情</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={taskDetail.status === "running"}
|
||||
onCheckedChange={toggleTaskStatus}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleCopy}>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={confirmDelete} className="text-red-500 hover:text-red-600">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -296,8 +260,8 @@ export default function MomentsSyncDetailPage({ params }: { params: { id: string
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h2 className="text-xl font-semibold">{taskDetail.name}</h2>
|
||||
<Badge variant={taskDetail.status === "running" ? "success" : "secondary"}>
|
||||
{taskDetail.status === "running" ? "进行中" : "已暂停"}
|
||||
<Badge variant={taskDetail.status == 1 ? "success" : "secondary"}>
|
||||
{taskDetail.status == 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,28 +287,24 @@ export default function MomentsSyncDetailPage({ params }: { params: { id: string
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="font-medium mb-1">账号类型</div>
|
||||
<div className="text-gray-600">{taskDetail.accountType === 1 ? "业务号" : "人设号"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">同步类型</div>
|
||||
<div className="text-gray-600">{taskDetail.syncType === 1 ? "循环同步" : "实时更新"}</div>
|
||||
<div className="text-gray-600">{taskDetail.config.accountType === 1 ? "业务号" : "人设号"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">同步间隔</div>
|
||||
<div className="text-gray-600">{taskDetail.syncInterval} 分钟</div>
|
||||
<div className="text-gray-600">{taskDetail.config.syncInterval} 分钟</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">每日同步数量</div>
|
||||
<div className="text-gray-600">{taskDetail.syncCount} 条</div>
|
||||
<div className="text-gray-600">{taskDetail.config.syncCount} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">允许发布时间段</div>
|
||||
<div className="text-gray-600">{taskDetail.startTime} - {taskDetail.endTime}</div>
|
||||
<div className="text-gray-600">{taskDetail.config.startTime} - {taskDetail.config.endTime}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">内容库</div>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{taskDetail.contentLibraries.map((lib) => (
|
||||
{taskDetail.config.contentLibraries.map((lib) => (
|
||||
<Badge key={lib.id} variant="outline" className="bg-blue-50">
|
||||
{lib.name}
|
||||
</Badge>
|
||||
@@ -357,24 +317,24 @@ export default function MomentsSyncDetailPage({ params }: { params: { id: string
|
||||
|
||||
<TabsContent value="devices" className="mt-4">
|
||||
<Card className="p-4">
|
||||
{taskDetail.devices.length === 0 ? (
|
||||
{taskDetail.config.deviceList.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">暂无关联设备</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{taskDetail.devices.map((device) => (
|
||||
{taskDetail.config.deviceList.map((device) => (
|
||||
<div key={device.id} className="flex items-center py-3 first:pt-0 last:pb-0">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
{device.avatar ? (
|
||||
<img src={device.avatar} alt={device.name} />
|
||||
<img src={device.avatar} alt={device.nickname} />
|
||||
) : (
|
||||
<div className="bg-blue-100 text-blue-600 h-full w-full flex items-center justify-center">
|
||||
{device.name.charAt(0)}
|
||||
{device.nickname}
|
||||
</div>
|
||||
)}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{device.name}</div>
|
||||
<div className="text-xs text-gray-500">ID: {device.id}</div>
|
||||
<div className="font-medium">{device.nickname}</div>
|
||||
<div className="text-xs text-gray-500">{device.alias || device.wechatId}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -40,7 +40,7 @@ Route::group('v1/', function () {
|
||||
Route::get('scenes-detail', 'app\cunkebao\controller\plan\GetPlanSceneListV1Controller@detail');
|
||||
Route::post('create', 'app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller@index');
|
||||
Route::get('list', 'app\cunkebao\controller\plan\PlanSceneV1Controller@index');
|
||||
Route::get('copy', 'app\cunkebao\controller\plan\PlanSceneV1Controller@copy');
|
||||
Route::get('copy', 'app\cunkebao\controller\plan\GetCreateAddFriendPlanV1Controller@copy');
|
||||
Route::delete('delete', 'app\cunkebao\controller\plan\PlanSceneV1Controller@delete');
|
||||
Route::post('updateStatus', 'app\cunkebao\controller\plan\PlanSceneV1Controller@updateStatus');
|
||||
Route::get('detail', 'app\cunkebao\controller\plan\GetAddFriendPlanDetailV1Controller@index');
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace app\cunkebao\controller;
|
||||
|
||||
use app\common\model\Device as DeviceModel;
|
||||
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
|
||||
use app\cunkebao\model\Workbench;
|
||||
use app\cunkebao\model\WorkbenchAutoLike;
|
||||
use app\cunkebao\model\WorkbenchMomentsSync;
|
||||
@@ -236,6 +238,11 @@ class WorkbenchController extends Controller
|
||||
$item->config = $item->momentsSync;
|
||||
$item->config->devices = json_decode($item->config->devices, true);
|
||||
$item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
|
||||
//同步记录
|
||||
$sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count();
|
||||
$item->syncCount = $sendNum;
|
||||
$lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime');
|
||||
$item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i',$lastTime) : '--';
|
||||
|
||||
// 获取内容库名称
|
||||
if (!empty($item->config->contentLibraries)) {
|
||||
@@ -426,6 +433,42 @@ class WorkbenchController extends Controller
|
||||
$workbench->config = $workbench->momentsSync;
|
||||
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
||||
$workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
|
||||
|
||||
//同步记录
|
||||
$sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count();
|
||||
$workbench->syncCount = $sendNum;
|
||||
$lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime');
|
||||
$workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i',$lastTime) : '--';
|
||||
|
||||
// 获取内容库名称
|
||||
if (!empty($workbench->config->contentLibraries)) {
|
||||
$libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentLibraries)
|
||||
->select();
|
||||
$workbench->config->contentLibraries = $libraryNames;
|
||||
} else {
|
||||
$workbench->config->contentLibraryNames = [];
|
||||
}
|
||||
|
||||
if(!empty($workbench->config->devices)){
|
||||
$deviceList = DeviceModel::alias('d')
|
||||
->field([
|
||||
'd.id', 'd.imei', 'd.memo', 'd.alive',
|
||||
'l.wechatId',
|
||||
'a.nickname', 'a.alias', 'a.avatar','a.alias'
|
||||
])
|
||||
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
|
||||
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
|
||||
->whereIn('d.id',$workbench->config->devices)
|
||||
->order('d.id desc')
|
||||
->select();
|
||||
$workbench->config->deviceList = $deviceList;
|
||||
}else{
|
||||
$workbench->config->deviceList = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
unset($workbench->momentsSync,$workbench->moments_sync);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -54,18 +54,18 @@ class PlanSceneV1Controller extends BaseController
|
||||
$val['acquiredCount'] = Db::name('task_customer')->where('task_id',$val['id'])->count();
|
||||
$val['addedCount'] = Db::name('task_customer')->where('task_id',$val['id'])->whereIn('status',[1,2,3,4])->count();
|
||||
$val['passCount'] = Db::name('task_customer')->where('task_id',$val['id'])->where('status',4)->count();
|
||||
|
||||
$val['passRate'] = 0;
|
||||
if(!empty($val['passCount']) && !empty($val['addedCount'])){
|
||||
$passRate = ($val['addedCount'] / $val['passCount']) * 100;
|
||||
$passRate = ($val['passCount'] / $val['addedCount']) * 100;
|
||||
$val['passRate'] = number_format($passRate,2);
|
||||
}
|
||||
|
||||
$lastTime = Db::name('task_customer')->where(['task_id'=>$val['id']])->max('updated_at');
|
||||
$val['lastUpdated'] = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
|
||||
|
||||
|
||||
}
|
||||
unset($val);
|
||||
|
||||
|
||||
|
||||
return ResponseHelper::success([
|
||||
'total' => $total,
|
||||
'list' => $list
|
||||
@@ -75,41 +75,6 @@ class PlanSceneV1Controller extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝计划任务
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function copy()
|
||||
{
|
||||
try {
|
||||
$params = $this->request->param();
|
||||
$planId = isset($params['planId']) ? intval($params['planId']) : 0;
|
||||
|
||||
if ($planId <= 0) {
|
||||
return ResponseHelper::error('计划ID不能为空', 400);
|
||||
}
|
||||
|
||||
$plan = Db::name('customer_acquisition_task')->where('id', $planId)->find();
|
||||
if (!$plan) {
|
||||
return ResponseHelper::error('计划不存在', 404);
|
||||
}
|
||||
|
||||
unset($plan['id']);
|
||||
$plan['name'] = $plan['name'] . ' (拷贝)';
|
||||
$plan['createTime'] = time();
|
||||
$plan['updateTime'] = time();
|
||||
|
||||
$newPlanId = Db::name('customer_acquisition_task')->insertGetId($plan);
|
||||
if (!$newPlanId) {
|
||||
return ResponseHelper::error('拷贝计划失败', 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success(['planId' => $newPlanId], '拷贝计划任务成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除计划任务
|
||||
@@ -163,4 +128,251 @@ class PlanSceneV1Controller extends BaseController
|
||||
return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取获客计划设备列表
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getPlanDevices()
|
||||
{
|
||||
try {
|
||||
$params = $this->request->param();
|
||||
$planId = isset($params['planId']) ? intval($params['planId']) : 0;
|
||||
$page = isset($params['page']) ? intval($params['page']) : 1;
|
||||
$limit = isset($params['limit']) ? intval($params['limit']) : 10;
|
||||
$deviceStatus = isset($params['deviceStatus']) ? $params['deviceStatus'] : '';
|
||||
$searchKeyword = isset($params['searchKeyword']) ? trim($params['searchKeyword']) : '';
|
||||
|
||||
// 验证计划ID
|
||||
if ($planId <= 0) {
|
||||
return ResponseHelper::error('计划ID不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证计划是否存在且用户有权限
|
||||
$plan = Db::name('customer_acquisition_task')
|
||||
->where([
|
||||
'id' => $planId,
|
||||
'deleteTime' => 0,
|
||||
'companyId' => $this->getUserInfo('companyId')
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$plan) {
|
||||
return ResponseHelper::error('计划不存在或无权限访问', 404);
|
||||
}
|
||||
|
||||
// 如果是管理员,需要验证用户权限
|
||||
if ($this->getUserInfo('isAdmin')) {
|
||||
$userPlan = Db::name('customer_acquisition_task')
|
||||
->where([
|
||||
'id' => $planId,
|
||||
'userId' => $this->getUserInfo('id')
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$userPlan) {
|
||||
return ResponseHelper::error('您没有权限访问该计划', 403);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
$where = [
|
||||
'pt.plan_id' => $planId,
|
||||
'd.deleteTime' => 0,
|
||||
'd.companyId' => $this->getUserInfo('companyId')
|
||||
];
|
||||
|
||||
// 设备状态筛选
|
||||
if (!empty($deviceStatus)) {
|
||||
$where['d.alive'] = $deviceStatus;
|
||||
}
|
||||
|
||||
// 搜索关键词
|
||||
$searchWhere = [];
|
||||
if (!empty($searchKeyword)) {
|
||||
$searchWhere[] = ['d.imei', 'like', "%{$searchKeyword}%"];
|
||||
$searchWhere[] = ['d.memo', 'like', "%{$searchKeyword}%"];
|
||||
}
|
||||
|
||||
// 查询设备总数
|
||||
$totalQuery = Db::name('plan_task_device')->alias('pt')
|
||||
->join('device d', 'pt.device_id = d.id')
|
||||
->where($where);
|
||||
|
||||
if (!empty($searchWhere)) {
|
||||
$totalQuery->where(function ($query) use ($searchWhere) {
|
||||
foreach ($searchWhere as $condition) {
|
||||
$query->whereOr($condition[0], $condition[1], $condition[2]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$total = $totalQuery->count();
|
||||
|
||||
// 查询设备列表
|
||||
$listQuery = Db::name('plan_task_device')->alias('pt')
|
||||
->join('device d', 'pt.device_id = d.id')
|
||||
->field([
|
||||
'd.id',
|
||||
'd.imei',
|
||||
'd.memo',
|
||||
'd.alive',
|
||||
'd.extra',
|
||||
'd.createTime',
|
||||
'd.updateTime',
|
||||
'pt.status as plan_device_status',
|
||||
'pt.createTime as assign_time'
|
||||
])
|
||||
->where($where)
|
||||
->order('pt.createTime', 'desc');
|
||||
|
||||
if (!empty($searchWhere)) {
|
||||
$listQuery->where(function ($query) use ($searchWhere) {
|
||||
foreach ($searchWhere as $condition) {
|
||||
$query->whereOr($condition[0], $condition[1], $condition[2]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$list = $listQuery->page($page, $limit)->select();
|
||||
|
||||
// 处理设备数据
|
||||
foreach ($list as &$device) {
|
||||
// 格式化时间
|
||||
$device['createTime'] = date('Y-m-d H:i:s', $device['createTime']);
|
||||
$device['updateTime'] = date('Y-m-d H:i:s', $device['updateTime']);
|
||||
$device['assign_time'] = date('Y-m-d H:i:s', $device['assign_time']);
|
||||
|
||||
// 解析设备额外信息
|
||||
if (!empty($device['extra'])) {
|
||||
$extra = json_decode($device['extra'], true);
|
||||
$device['battery'] = isset($extra['battery']) ? intval($extra['battery']) : 0;
|
||||
$device['device_info'] = $extra;
|
||||
} else {
|
||||
$device['battery'] = 0;
|
||||
$device['device_info'] = [];
|
||||
}
|
||||
|
||||
// 设备状态文本
|
||||
$device['alive_text'] = $this->getDeviceStatusText($device['alive']);
|
||||
$device['plan_device_status_text'] = $this->getPlanDeviceStatusText($device['plan_device_status']);
|
||||
|
||||
// 获取设备当前微信登录信息
|
||||
$wechatLogin = Db::name('device_wechat_login')
|
||||
->where([
|
||||
'deviceId' => $device['id'],
|
||||
'companyId' => $this->getUserInfo('companyId'),
|
||||
'alive' => 1
|
||||
])
|
||||
->order('createTime', 'desc')
|
||||
->find();
|
||||
|
||||
$device['current_wechat'] = $wechatLogin ? [
|
||||
'wechatId' => $wechatLogin['wechatId'],
|
||||
'nickname' => $wechatLogin['nickname'] ?? '',
|
||||
'loginTime' => date('Y-m-d H:i:s', $wechatLogin['createTime'])
|
||||
] : null;
|
||||
|
||||
// 获取设备在该计划中的任务统计
|
||||
$device['task_stats'] = $this->getDeviceTaskStats($device['id'], $planId);
|
||||
|
||||
// 移除原始extra字段
|
||||
unset($device['extra']);
|
||||
}
|
||||
unset($device);
|
||||
|
||||
return ResponseHelper::success([
|
||||
'total' => $total,
|
||||
'list' => $list,
|
||||
'plan_info' => [
|
||||
'id' => $plan['id'],
|
||||
'name' => $plan['name'],
|
||||
'status' => $plan['status']
|
||||
]
|
||||
], '获取计划设备列表成功');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态文本
|
||||
*
|
||||
* @param int $status
|
||||
* @return string
|
||||
*/
|
||||
private function getDeviceStatusText($status)
|
||||
{
|
||||
$statusMap = [
|
||||
0 => '离线',
|
||||
1 => '在线',
|
||||
2 => '忙碌',
|
||||
3 => '故障'
|
||||
];
|
||||
return isset($statusMap[$status]) ? $statusMap[$status] : '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划设备状态文本
|
||||
*
|
||||
* @param int $status
|
||||
* @return string
|
||||
*/
|
||||
private function getPlanDeviceStatusText($status)
|
||||
{
|
||||
$statusMap = [
|
||||
0 => '待分配',
|
||||
1 => '已分配',
|
||||
2 => '执行中',
|
||||
3 => '已完成',
|
||||
4 => '已暂停',
|
||||
5 => '已取消'
|
||||
];
|
||||
return isset($statusMap[$status]) ? $statusMap[$status] : '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备在指定计划中的任务统计
|
||||
*
|
||||
* @param int $deviceId
|
||||
* @param int $planId
|
||||
* @return array
|
||||
*/
|
||||
private function getDeviceTaskStats($deviceId, $planId)
|
||||
{
|
||||
// 获取该设备在计划中的任务总数
|
||||
$totalTasks = Db::name('task_customer')
|
||||
->where([
|
||||
'task_id' => $planId,
|
||||
'device_id' => $deviceId
|
||||
])
|
||||
->count();
|
||||
|
||||
// 获取已完成的任务数
|
||||
$completedTasks = Db::name('task_customer')
|
||||
->where([
|
||||
'task_id' => $planId,
|
||||
'device_id' => $deviceId,
|
||||
'status' => 4
|
||||
])
|
||||
->count();
|
||||
|
||||
// 获取进行中的任务数
|
||||
$processingTasks = Db::name('task_customer')
|
||||
->where([
|
||||
'task_id' => $planId,
|
||||
'device_id' => $deviceId,
|
||||
'status' => ['in', [1, 2, 3]]
|
||||
])
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total_tasks' => $totalTasks,
|
||||
'completed_tasks' => $completedTasks,
|
||||
'processing_tasks' => $processingTasks,
|
||||
'completion_rate' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ class PostCreateAddFriendPlanV1Controller extends Controller
|
||||
|
||||
|
||||
try {
|
||||
Db::startTrans();
|
||||
// 插入数据
|
||||
$planId = Db::name('customer_acquisition_task')->insertGetId($data);
|
||||
|
||||
@@ -220,7 +221,7 @@ class PostCreateAddFriendPlanV1Controller extends Controller
|
||||
$existingPhones = [];
|
||||
if (!empty($phones)) {
|
||||
$existing = Db::name('task_customer')
|
||||
->where('task_id', $params['planId'])
|
||||
->where('task_id', $planId)
|
||||
->where('phone', 'in', $phones)
|
||||
->field('phone')
|
||||
->select();
|
||||
@@ -233,7 +234,7 @@ class PostCreateAddFriendPlanV1Controller extends Controller
|
||||
$phone = !empty($row['phone']) ? $row['phone'] : $row['wechat'];
|
||||
if (!empty($phone) && !in_array($phone, $existingPhones)) {
|
||||
$newData[] = [
|
||||
'task_id' => $params['planId'],
|
||||
'task_id' => $planId,
|
||||
'name' => $row['name'] ?? '',
|
||||
'source' => $row['source'] ?? '',
|
||||
'phone' => $phone,
|
||||
@@ -254,6 +255,7 @@ class PostCreateAddFriendPlanV1Controller extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
|
||||
return ResponseHelper::success(['planId' => $planId], '添加计划任务成功');
|
||||
|
||||
|
||||
@@ -153,14 +153,16 @@ class Adapter implements WeChatServiceInterface
|
||||
|
||||
public function handleCustomerTaskWithStatusIsNew(int $current_worker_id, int $process_count_for_status_0)
|
||||
{
|
||||
$tasks = Db::name('task_customer')
|
||||
->where('status', 0)
|
||||
->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}")
|
||||
->limit(50)
|
||||
|
||||
$tasks = Db::name('customer_acquisition_task')->alias('task')
|
||||
->join('task_customer customer','task.id=customer.task_id')
|
||||
->where(['task.status' => 1,'customer.status'=>0,'task.deleteTime' => 0])
|
||||
->whereRaw("customer.id % $process_count_for_status_0 = {$current_worker_id}")
|
||||
->order('id DESC')
|
||||
->select();
|
||||
|
||||
|
||||
|
||||
if ($tasks) {
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
|
||||
Reference in New Issue
Block a user