门店端适配新表 及操盘手端工作台优化
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,3 +6,7 @@ Backend/dist
|
|||||||
Backend/node_modules
|
Backend/node_modules
|
||||||
Store_vue/node_modules
|
Store_vue/node_modules
|
||||||
Store_vue/unpackage
|
Store_vue/unpackage
|
||||||
|
Server/.specstory/
|
||||||
|
Store_vue/.specstory/
|
||||||
|
*.zip
|
||||||
|
*.cursorindexingignore
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface ContentLibrary {
|
|||||||
avatar: string
|
avatar: string
|
||||||
}[]
|
}[]
|
||||||
creator: string
|
creator: string
|
||||||
|
creatorName?: string
|
||||||
itemCount: number
|
itemCount: number
|
||||||
lastUpdated: string
|
lastUpdated: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -74,7 +75,7 @@ export default function ContentLibraryPage() {
|
|||||||
|
|
||||||
if (response.code === 200 && response.data) {
|
if (response.code === 200 && response.data) {
|
||||||
// 转换数据格式以匹配原有UI
|
// 转换数据格式以匹配原有UI
|
||||||
const transformedLibraries = response.data.list.map((item) => {
|
const transformedLibraries = response.data.list.map((item: any) => {
|
||||||
const transformedItem: ContentLibrary = {
|
const transformedItem: ContentLibrary = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -83,7 +84,8 @@ export default function ContentLibraryPage() {
|
|||||||
...(item.sourceFriends || []).map((id: string) => ({ id, nickname: `好友${id}`, avatar: "/placeholder.svg" })),
|
...(item.sourceFriends || []).map((id: string) => ({ id, nickname: `好友${id}`, avatar: "/placeholder.svg" })),
|
||||||
...(item.sourceGroups || []).map((id: string) => ({ id, nickname: `群组${id}`, avatar: "/placeholder.svg" }))
|
...(item.sourceGroups || []).map((id: string) => ({ id, nickname: `群组${id}`, avatar: "/placeholder.svg" }))
|
||||||
],
|
],
|
||||||
creator: item.creator || "系统",
|
creator: item.creatorName || "系统",
|
||||||
|
creatorName: item.creatorName,
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
lastUpdated: item.updateTime,
|
lastUpdated: item.updateTime,
|
||||||
enabled: item.isEnabled === 1,
|
enabled: item.isEnabled === 1,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, use } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { ChevronLeft, Search } from "lucide-react"
|
import { ChevronLeft, Search } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -38,7 +38,8 @@ interface Task {
|
|||||||
config: TaskConfig
|
config: TaskConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditAutoLikePage({ params }: { params: { id: string } }) {
|
export default function EditAutoLikePage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const resolvedParams = use(params)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [currentStep, setCurrentStep] = useState(1)
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||||
@@ -62,7 +63,7 @@ export default function EditAutoLikePage({ params }: { params: { id: string } })
|
|||||||
const fetchTaskDetail = async () => {
|
const fetchTaskDetail = async () => {
|
||||||
const loadingToast = showToast("正在加载任务信息...", "loading", true);
|
const loadingToast = showToast("正在加载任务信息...", "loading", true);
|
||||||
try {
|
try {
|
||||||
const response = await api.get<{code: number, msg: string, data: Task}>(`/v1/workbench/detail?id=${params.id}`)
|
const response = await api.get<{code: number, msg: string, data: Task}>(`/v1/workbench/detail?id=${resolvedParams.id}`)
|
||||||
|
|
||||||
if (response.code === 200 && response.data) {
|
if (response.code === 200 && response.data) {
|
||||||
const task = response.data
|
const task = response.data
|
||||||
@@ -110,7 +111,7 @@ export default function EditAutoLikePage({ params }: { params: { id: string } })
|
|||||||
const loadingToast = showToast("正在更新任务...", "loading", true);
|
const loadingToast = showToast("正在更新任务...", "loading", true);
|
||||||
try {
|
try {
|
||||||
const response = await api.post<ApiResponse>('/v1/workbench/update', {
|
const response = await api.post<ApiResponse>('/v1/workbench/update', {
|
||||||
id: params.id,
|
id: resolvedParams.id,
|
||||||
type: 1,
|
type: 1,
|
||||||
name: formData.taskName,
|
name: formData.taskName,
|
||||||
interval: formData.likeInterval,
|
interval: formData.likeInterval,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, use } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { ChevronLeft, Search } from "lucide-react"
|
import { ChevronLeft, Search } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -12,18 +12,23 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { api, ApiResponse } from "@/lib/api"
|
import { api, ApiResponse } from "@/lib/api"
|
||||||
import { showToast } from "@/lib/toast"
|
import { showToast } from "@/lib/toast"
|
||||||
|
|
||||||
// 定义基本设置表单数据类型,与BasicSettings组件的formData类型匹配
|
interface Task {
|
||||||
interface BasicSettingsFormData {
|
id: string
|
||||||
taskName: string
|
name: string
|
||||||
startTime: string
|
status: number
|
||||||
endTime: string
|
config: {
|
||||||
syncCount: number
|
startTime: string
|
||||||
syncInterval: number
|
endTime: string
|
||||||
accountType: "business" | "personal"
|
syncCount: number
|
||||||
enabled: boolean
|
syncInterval: number
|
||||||
|
syncType: number
|
||||||
|
devices: string[]
|
||||||
|
contentLibraries: string[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditMomentsSyncPage({ params }: { params: { id: string } }) {
|
export default function EditMomentsSyncPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const resolvedParams = use(params)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [currentStep, setCurrentStep] = useState(1)
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||||
@@ -34,31 +39,30 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
|
|||||||
startTime: "06:00",
|
startTime: "06:00",
|
||||||
endTime: "23:59",
|
endTime: "23:59",
|
||||||
syncCount: 5,
|
syncCount: 5,
|
||||||
syncInterval: 30, // 同步间隔,默认30分钟
|
syncInterval: 30,
|
||||||
accountType: "business" as "business" | "personal",
|
accountType: "business" as "business" | "personal",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
selectedDevices: [] as string[],
|
selectedDevices: [] as string[],
|
||||||
selectedLibraries: [] as string[],
|
selectedLibraries: [] as string[],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取任务详情
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTaskDetail = async () => {
|
const fetchTaskDetail = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await api.get<ApiResponse>(`/v1/workbench/detail?id=${params.id}`)
|
const response = await api.get<{code: number, msg: string, data: Task}>(`/v1/workbench/detail?id=${resolvedParams.id}`)
|
||||||
if (response.code === 200 && response.data) {
|
if (response.code === 200 && response.data) {
|
||||||
const taskData = response.data
|
const taskData = response.data
|
||||||
setFormData({
|
setFormData({
|
||||||
taskName: taskData.name || "",
|
taskName: taskData.name || "",
|
||||||
startTime: taskData.startTime || "06:00",
|
startTime: taskData.config.startTime || "06:00",
|
||||||
endTime: taskData.endTime || "23:59",
|
endTime: taskData.config.endTime || "23:59",
|
||||||
syncCount: taskData.syncCount || 5,
|
syncCount: taskData.config.syncCount || 5,
|
||||||
syncInterval: taskData.syncInterval || 30,
|
syncInterval: taskData.config.syncInterval || 30,
|
||||||
accountType: taskData.syncType === 1 ? "business" : "personal",
|
accountType: taskData.config.syncType === 1 ? "business" : "personal",
|
||||||
enabled: !!taskData.enabled,
|
enabled: !!taskData.status,
|
||||||
selectedDevices: taskData.devices || [],
|
selectedDevices: taskData.config.devices || [],
|
||||||
selectedLibraries: taskData.contentLibraries || [],
|
selectedLibraries: taskData.config.contentLibraries || [],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
showToast(response.msg || "获取任务详情失败", "error")
|
showToast(response.msg || "获取任务详情失败", "error")
|
||||||
@@ -74,17 +78,12 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchTaskDetail()
|
fetchTaskDetail()
|
||||||
}, [params.id, router])
|
}, [resolvedParams.id, router])
|
||||||
|
|
||||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||||
setFormData((prev) => ({ ...prev, ...data }))
|
setFormData((prev) => ({ ...prev, ...data }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 专门用于基本设置的更新函数
|
|
||||||
const handleBasicSettingsUpdate = (data: Partial<BasicSettingsFormData>) => {
|
|
||||||
setFormData((prev) => ({ ...prev, ...data }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
setCurrentStep((prev) => Math.min(prev + 1, 3))
|
setCurrentStep((prev) => Math.min(prev + 1, 3))
|
||||||
}
|
}
|
||||||
@@ -96,16 +95,16 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
|
|||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.post<ApiResponse>('/v1/workbench/update', {
|
const response = await api.post<ApiResponse>('/v1/workbench/update', {
|
||||||
id: params.id,
|
id: resolvedParams.id,
|
||||||
type: 2, // 朋友圈同步任务类型为2
|
type: 2,
|
||||||
name: formData.taskName,
|
name: formData.taskName,
|
||||||
syncInterval: formData.syncInterval,
|
syncInterval: formData.syncInterval,
|
||||||
syncCount: formData.syncCount,
|
syncCount: formData.syncCount,
|
||||||
syncType: formData.accountType === "business" ? 1 : 2, // 业务号为1,人设号为2
|
syncType: formData.accountType === "business" ? 1 : 2,
|
||||||
startTime: formData.startTime,
|
startTime: formData.startTime,
|
||||||
endTime: formData.endTime,
|
endTime: formData.endTime,
|
||||||
accountType: formData.accountType === "business" ? 1 : 2,
|
accountType: formData.accountType === "business" ? 1 : 2,
|
||||||
status: formData.enabled ? 1 : 0, // 状态:0=禁用,1=启用
|
status: formData.enabled ? 1 : 0,
|
||||||
devices: formData.selectedDevices,
|
devices: formData.selectedDevices,
|
||||||
contentLibraries: formData.selectedLibraries
|
contentLibraries: formData.selectedLibraries
|
||||||
});
|
});
|
||||||
@@ -150,16 +149,8 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
|
|||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<BasicSettings
|
<BasicSettings
|
||||||
formData={{
|
formData={formData}
|
||||||
taskName: formData.taskName,
|
onChange={handleUpdateFormData}
|
||||||
startTime: formData.startTime,
|
|
||||||
endTime: formData.endTime,
|
|
||||||
syncCount: formData.syncCount,
|
|
||||||
syncInterval: formData.syncInterval,
|
|
||||||
accountType: formData.accountType,
|
|
||||||
enabled: formData.enabled
|
|
||||||
}}
|
|
||||||
onChange={handleBasicSettingsUpdate}
|
|
||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -249,21 +240,6 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="fixed bottom-0 left-0 right-0 h-16 bg-white border-t flex items-center justify-around px-6">
|
|
||||||
<button className="flex flex-col items-center text-blue-600">
|
|
||||||
<span className="text-sm mt-1">首页</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex flex-col items-center text-gray-400">
|
|
||||||
<span className="text-sm mt-1">场景获客</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex flex-col items-center text-gray-400">
|
|
||||||
<span className="text-sm mt-1">工作台</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex flex-col items-center text-gray-400">
|
|
||||||
<span className="text-sm mt-1">我的</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Switch } from "@/components/ui/switch"
|
|||||||
import { Plus, Minus, Clock, HelpCircle } from "lucide-react"
|
import { Plus, Minus, Clock, HelpCircle } from "lucide-react"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { useViewMode } from "@/app/components/LayoutWrapper"
|
import { useViewMode } from "@/app/components/LayoutWrapper"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
interface BasicSettingsProps {
|
interface BasicSettingsProps {
|
||||||
formData: {
|
formData: {
|
||||||
@@ -24,163 +25,142 @@ interface BasicSettingsProps {
|
|||||||
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
||||||
const { viewMode } = useViewMode()
|
const { viewMode } = useViewMode()
|
||||||
|
|
||||||
|
const handleSyncCountChange = (delta: number) => {
|
||||||
|
const newValue = Math.max(1, formData.syncCount + delta)
|
||||||
|
onChange({ syncCount: newValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSyncIntervalChange = (delta: number) => {
|
||||||
|
const newValue = Math.max(5, formData.syncInterval + delta)
|
||||||
|
onChange({ syncInterval: newValue })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 ${viewMode === "desktop" ? "p-6" : "p-4"}`}>
|
<div className={`space-y-6 ${viewMode === "desktop" ? "p-6" : "p-4"}`}>
|
||||||
<div className={`grid ${viewMode === "desktop" ? "grid-cols-2 gap-8" : "grid-cols-1 gap-4"}`}>
|
<div className={`grid ${viewMode === "desktop" ? "grid-cols-2 gap-8" : "grid-cols-1 gap-4"}`}>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-medium mb-2">任务名称</div>
|
<Label htmlFor="taskName" className="text-base">任务名称</Label>
|
||||||
<Input
|
<Input
|
||||||
|
id="taskName"
|
||||||
value={formData.taskName}
|
value={formData.taskName}
|
||||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||||
placeholder="请输入任务名称"
|
placeholder="请输入任务名称"
|
||||||
className="h-12 border-0 border-b border-gray-200 rounded-none focus-visible:ring-0 focus-visible:border-blue-600 px-0 text-base"
|
className="mt-1.5 h-12 rounded-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-medium mb-2">允许发布时间段</div>
|
<Label className="text-base">每日同步数量</Label>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4 mt-1.5">
|
||||||
<div className="relative flex-1">
|
<Button
|
||||||
<Input
|
type="button"
|
||||||
type="time"
|
variant="outline"
|
||||||
value={formData.startTime}
|
size="icon"
|
||||||
onChange={(e) => onChange({ startTime: e.target.value })}
|
className="h-12 w-12 rounded-xl"
|
||||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
onClick={() => handleSyncCountChange(-1)}
|
||||||
/>
|
>
|
||||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
<Minus className="h-4 w-4" />
|
||||||
</div>
|
</Button>
|
||||||
<span className="text-gray-500">至</span>
|
<div className="w-20 text-center text-base">{formData.syncCount}</div>
|
||||||
<div className="relative flex-1">
|
<Button
|
||||||
<Input
|
type="button"
|
||||||
type="time"
|
variant="outline"
|
||||||
value={formData.endTime}
|
size="icon"
|
||||||
onChange={(e) => onChange({ endTime: e.target.value })}
|
className="h-12 w-12 rounded-xl"
|
||||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
onClick={() => handleSyncCountChange(1)}
|
||||||
/>
|
>
|
||||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
<Plus className="h-4 w-4" />
|
||||||
</div>
|
</Button>
|
||||||
|
<span className="text-gray-500">条/天</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-medium mb-2">每日同步数量</div>
|
<Label className="text-base">同步间隔</Label>
|
||||||
<div className="flex items-center space-x-5">
|
<div className="flex items-center space-x-4 mt-1.5">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="icon"
|
||||||
onClick={() => onChange({ syncCount: Math.max(1, formData.syncCount - 1) })}
|
className="h-12 w-12 rounded-xl"
|
||||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
onClick={() => handleSyncIntervalChange(-5)}
|
||||||
>
|
>
|
||||||
<Minus className="h-5 w-5" />
|
<Minus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="w-8 text-center text-lg font-medium">{formData.syncCount}</span>
|
<div className="w-20 text-center text-base">{formData.syncInterval}</div>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="icon"
|
||||||
onClick={() => onChange({ syncCount: formData.syncCount + 1 })}
|
className="h-12 w-12 rounded-xl"
|
||||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
onClick={() => handleSyncIntervalChange(5)}
|
||||||
>
|
>
|
||||||
<Plus className="h-5 w-5" />
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
<span className="text-gray-500">条朋友圈</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-base font-medium mb-2">同步间隔</div>
|
|
||||||
<div className="flex items-center space-x-5">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
onClick={() => onChange({ syncInterval: Math.max(10, formData.syncInterval - 10) })}
|
|
||||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
|
||||||
>
|
|
||||||
<Minus className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<span className="w-8 text-center text-lg font-medium">{formData.syncInterval}</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
onClick={() => onChange({ syncInterval: Math.min(120, formData.syncInterval + 10) })}
|
|
||||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-gray-500">分钟</span>
|
<span className="text-gray-500">分钟</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 mt-1.5">设置每次发朋友圈的时间间隔</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-medium mb-2">账号类型</div>
|
<Label className="text-base">同步时间</Label>
|
||||||
<div className="flex space-x-4">
|
<div className="grid grid-cols-2 gap-4 mt-1.5">
|
||||||
<div className="flex-1 relative">
|
<Input
|
||||||
<TooltipProvider>
|
type="time"
|
||||||
<Tooltip>
|
value={formData.startTime}
|
||||||
<TooltipTrigger asChild>
|
onChange={(e) => onChange({ startTime: e.target.value })}
|
||||||
<Button
|
className="h-12 rounded-xl"
|
||||||
variant="ghost"
|
/>
|
||||||
onClick={() => onChange({ accountType: "business" })}
|
<Input
|
||||||
className={`w-full h-12 justify-between rounded-lg ${
|
type="time"
|
||||||
formData.accountType === "business"
|
value={formData.endTime}
|
||||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
onChange={(e) => onChange({ endTime: e.target.value })}
|
||||||
: "bg-white hover:bg-gray-50"
|
className="h-12 rounded-xl"
|
||||||
}`}
|
/>
|
||||||
>
|
|
||||||
业务号
|
|
||||||
<HelpCircle
|
|
||||||
className={`h-4 w-4 ${formData.accountType === "business" ? "text-white/70" : "text-gray-400"}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-[300px]">
|
|
||||||
<p>
|
|
||||||
业务号能够循环推送内容库中的内容。当内容库所有内容循环推送完毕后,若有新内容则优先推送新内容,若无新内容则继续循环推送。
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 relative">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onChange({ accountType: "personal" })}
|
|
||||||
className={`w-full h-12 justify-between rounded-lg ${
|
|
||||||
formData.accountType === "personal"
|
|
||||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
|
||||||
: "bg-white hover:bg-gray-50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
人设号
|
|
||||||
<HelpCircle
|
|
||||||
className={`h-4 w-4 ${formData.accountType === "personal" ? "text-white/70" : "text-gray-400"}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>用于实时更新同步,有新动态时进行同步,无动态则不同步。</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-2">
|
<div>
|
||||||
<span className="text-base font-medium">是否启用</span>
|
<Label className="text-base">账号类型</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-4 mt-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-12 rounded-xl border ${
|
||||||
|
formData.accountType === "business"
|
||||||
|
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||||
|
: "border-gray-200 text-gray-600"
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange({ accountType: "business" })}
|
||||||
|
>
|
||||||
|
业务号
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-12 rounded-xl border ${
|
||||||
|
formData.accountType === "personal"
|
||||||
|
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||||
|
: "border-gray-200 text-gray-600"
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange({ accountType: "personal" })}
|
||||||
|
>
|
||||||
|
人设号
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-base">是否启用</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={formData.enabled}
|
checked={formData.enabled}
|
||||||
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
||||||
className="data-[state=checked]:bg-blue-600 h-7 w-12"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base font-medium shadow-sm"
|
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||||
|
disabled={!formData.taskName}
|
||||||
>
|
>
|
||||||
下一步
|
下一步
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Search, RefreshCw, Loader2 } from "lucide-react"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Search, CheckCircle2, Circle } from "lucide-react"
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { api } from "@/lib/api"
|
||||||
|
import { showToast } from "@/lib/toast"
|
||||||
|
|
||||||
|
interface ContentLibrary {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
sourceType: number
|
||||||
|
creatorName: string
|
||||||
|
updateTime: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
interface ContentLibrarySelectionDialogProps {
|
interface ContentLibrarySelectionDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -21,93 +36,162 @@ export function ContentLibrarySelectionDialog({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: ContentLibrarySelectionDialogProps) {
|
}: ContentLibrarySelectionDialogProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [libraries] = useState([
|
const [loading, setLoading] = useState(false)
|
||||||
{ id: "1", name: "卡若朋友圈", count: 58 },
|
const [libraries, setLibraries] = useState<ContentLibrary[]>([])
|
||||||
{ id: "2", name: "暗黑4代练", count: 422 },
|
const [tempSelected, setTempSelected] = useState<string[]>([])
|
||||||
{ id: "3", name: "家装设计", count: 107 },
|
|
||||||
{ id: "4", name: "美食分享", count: 321 },
|
|
||||||
{ id: "5", name: "旅游攻略", count: 89 },
|
|
||||||
])
|
|
||||||
|
|
||||||
const [tempSelectedLibraries, setTempSelectedLibraries] = useState<string[]>(selectedLibraries)
|
// 获取内容库列表
|
||||||
|
const fetchLibraries = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
...(searchQuery ? { keyword: searchQuery } : {})
|
||||||
|
})
|
||||||
|
const response = await api.get<{
|
||||||
|
code: number
|
||||||
|
msg: string
|
||||||
|
data: {
|
||||||
|
list: ContentLibrary[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
}>(`/v1/content/library/list?${queryParams.toString()}`)
|
||||||
|
|
||||||
const toggleLibrary = (libraryId: string) => {
|
if (response.code === 200 && response.data) {
|
||||||
setTempSelectedLibraries((prev) =>
|
setLibraries(response.data.list)
|
||||||
prev.includes(libraryId) ? prev.filter((id) => id !== libraryId) : [...prev, libraryId]
|
} else {
|
||||||
|
showToast(response.msg || "获取内容库列表失败", "error")
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("获取内容库列表失败:", error)
|
||||||
|
showToast(error?.message || "请检查网络连接", "error")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchLibraries()
|
||||||
|
setTempSelected(selectedLibraries)
|
||||||
|
}
|
||||||
|
}, [open, searchQuery, selectedLibraries])
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchLibraries()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (tempSelected.length === libraries.length) {
|
||||||
|
setTempSelected([])
|
||||||
|
} else {
|
||||||
|
setTempSelected(libraries.map(lib => lib.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLibraryToggle = (libraryId: string) => {
|
||||||
|
setTempSelected(prev =>
|
||||||
|
prev.includes(libraryId)
|
||||||
|
? prev.filter(id => id !== libraryId)
|
||||||
|
: [...prev, libraryId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleDialogOpenChange = (open: boolean) => {
|
||||||
onSelect(tempSelectedLibraries)
|
if (!open) {
|
||||||
onOpenChange(false)
|
setTempSelected(selectedLibraries)
|
||||||
|
}
|
||||||
|
onOpenChange(open)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setTempSelectedLibraries(selectedLibraries)
|
|
||||||
onOpenChange(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredLibraries = libraries.filter((library) =>
|
|
||||||
library.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||||
<DialogContent className="max-w-md p-0 overflow-hidden">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader className="px-4 py-3 border-b">
|
<DialogHeader>
|
||||||
<DialogTitle>选择内容库</DialogTitle>
|
<DialogTitle>选择内容库</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="flex items-center space-x-2 my-4">
|
||||||
<div className="relative mb-4">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索内容库"
|
placeholder="搜索内容库"
|
||||||
className="pl-10"
|
className="pl-9"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="h-[300px] pr-4">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="space-y-2">
|
<div className="text-sm text-gray-500">
|
||||||
{filteredLibraries.map((library) => (
|
已选择 {tempSelected.length} 个内容库
|
||||||
<div
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
disabled={loading || libraries.length === 0}
|
||||||
|
>
|
||||||
|
{tempSelected.length === libraries.length ? "取消全选" : "全选"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="h-[400px] -mx-6 px-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
) : libraries.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
libraries.map((library) => (
|
||||||
|
<Label
|
||||||
key={library.id}
|
key={library.id}
|
||||||
className="flex items-center justify-between p-3 rounded-lg border hover:bg-gray-50 cursor-pointer"
|
className="flex items-center justify-between p-4 rounded-lg border hover:bg-gray-50 cursor-pointer"
|
||||||
onClick={() => toggleLibrary(library.id)}
|
htmlFor={library.id}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="flex items-center space-x-3 flex-1 min-w-0 pr-4">
|
||||||
<h3 className="font-medium">{library.name}</h3>
|
<Checkbox
|
||||||
<p className="text-sm text-gray-500">{library.count}条内容</p>
|
id={library.id}
|
||||||
|
checked={tempSelected.includes(library.id)}
|
||||||
|
onCheckedChange={() => handleLibraryToggle(library.id)}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="font-medium truncate mb-1">{library.name}</div>
|
||||||
|
<div className="text-sm text-gray-500 truncate mb-1">创建人:{library.creatorName}</div>
|
||||||
|
<div className="text-sm text-gray-500 truncate">更新时间:{new Date(library.updateTime).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{tempSelectedLibraries.includes(library.id) ? (
|
{/* <Badge variant={library.status === 1 ? "default" : "secondary"}>
|
||||||
<CheckCircle2 className="h-6 w-6 text-blue-500" />
|
{library.status === 1 ? "启用" : "已停用"}
|
||||||
) : (
|
</Badge> */}
|
||||||
<Circle className="h-6 w-6 text-gray-300" />
|
</Label>
|
||||||
)}
|
))
|
||||||
</div>
|
)}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex border-t p-4">
|
<DialogFooter className="mt-4">
|
||||||
<Button
|
{/* <Button variant="outline" onClick={() => handleDialogOpenChange(false)}>
|
||||||
variant="outline"
|
|
||||||
className="flex-1 mr-2"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
取消
|
取消
|
||||||
|
</Button> */}
|
||||||
|
<Button onClick={() => {
|
||||||
|
onSelect(tempSelected)
|
||||||
|
onOpenChange(false)
|
||||||
|
}}>
|
||||||
|
确定{tempSelected.length > 0 ? ` (${tempSelected.length})` : ''}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</DialogFooter>
|
||||||
className="flex-1"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={tempSelectedLibraries.length === 0}
|
|
||||||
>
|
|
||||||
确定 ({tempSelectedLibraries.length})
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,58 +1,51 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { Search, RefreshCw, Loader2 } from "lucide-react"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
import { Search, RefreshCw, Loader2 } from "lucide-react"
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { api } from "@/lib/api"
|
import { api } from "@/lib/api"
|
||||||
import { showToast } from "@/lib/toast"
|
import { showToast } from "@/lib/toast"
|
||||||
|
|
||||||
interface ServerDevice {
|
|
||||||
id: number
|
|
||||||
imei: string
|
|
||||||
memo: string
|
|
||||||
wechatId: string
|
|
||||||
alive: number
|
|
||||||
totalFriend: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
imei: string
|
imei: string
|
||||||
wxid: string
|
wechatId: string
|
||||||
status: "online" | "offline"
|
memo?: string
|
||||||
totalFriend: number
|
alive: number
|
||||||
|
usedInPlans: number
|
||||||
|
lastActiveTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceSelectionDialogProps {
|
interface DeviceSelectionDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
selectedDevices: number[]
|
selectedDevices: string[]
|
||||||
onSelect: (devices: number[]) => void
|
onSelect: (devices: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) {
|
export function DeviceSelectionDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
selectedDevices,
|
||||||
|
onSelect,
|
||||||
|
}: DeviceSelectionDialogProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [statusFilter, setStatusFilter] = useState("all")
|
const [statusFilter, setStatusFilter] = useState("all")
|
||||||
const [devices, setDevices] = useState<Device[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [tempSelectedDevices, setTempSelectedDevices] = useState<number[]>(selectedDevices)
|
const [devices, setDevices] = useState<Device[]>([])
|
||||||
|
const [tempSelected, setTempSelected] = useState<string[]>([])
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setTempSelectedDevices(selectedDevices)
|
|
||||||
fetchDevices()
|
|
||||||
}
|
|
||||||
}, [open, selectedDevices])
|
|
||||||
|
|
||||||
|
// 获取设备列表
|
||||||
const fetchDevices = async () => {
|
const fetchDevices = async () => {
|
||||||
const loadingToast = showToast("正在加载设备列表...", "loading", true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const response = await api.get<{code: number, msg: string, data: {list: ServerDevice[], total: number}}>('/v1/devices?page=1&limit=100')
|
const response = await api.get<{code: number, msg: string, data: {list: ServerDevice[], total: number}}>('/v1/devices?page=1&limit=100')
|
||||||
@@ -71,58 +64,75 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
|
|||||||
showToast(response.msg || "获取设备列表失败", "error")
|
showToast(response.msg || "获取设备列表失败", "error")
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('获取设备列表失败:', error)
|
console.error("获取设备列表失败:", error)
|
||||||
showToast(error?.message || "请检查网络连接", "error")
|
showToast(error?.message || "请检查网络连接", "error")
|
||||||
} finally {
|
} finally {
|
||||||
loadingToast.remove();
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchDevices()
|
||||||
|
setTempSelected(selectedDevices)
|
||||||
|
}
|
||||||
|
}, [open, searchQuery, selectedDevices])
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
fetchDevices()
|
fetchDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeviceToggle = (deviceId: number, checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
setTempSelectedDevices(prev => [...prev, deviceId])
|
|
||||||
} else {
|
|
||||||
setTempSelectedDevices(prev => prev.filter(id => id !== deviceId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
onSelect(tempSelectedDevices)
|
|
||||||
onOpenChange(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤设备列表
|
|
||||||
const filteredDevices = devices.filter(device => {
|
const filteredDevices = devices.filter(device => {
|
||||||
const matchesSearch = searchQuery === "" ||
|
const matchesSearch = !searchQuery ||
|
||||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
device.wxid.toLowerCase().includes(searchQuery.toLowerCase())
|
device.wechatId.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
|
||||||
const matchesStatus = statusFilter === "all" || device.status === statusFilter
|
const matchesStatus = statusFilter === "all" ||
|
||||||
|
(statusFilter === "online" && device.alive === 1) ||
|
||||||
|
(statusFilter === "offline" && device.alive === 0)
|
||||||
|
|
||||||
return matchesSearch && matchesStatus
|
return matchesSearch && matchesStatus
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleDialogOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
setTempSelected(selectedDevices)
|
||||||
|
}
|
||||||
|
onOpenChange(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (tempSelected.length === filteredDevices.length) {
|
||||||
|
setTempSelected([])
|
||||||
|
} else {
|
||||||
|
setTempSelected(filteredDevices.map(device => device.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeviceToggle = (deviceId: string) => {
|
||||||
|
setTempSelected(prev =>
|
||||||
|
prev.includes(deviceId)
|
||||||
|
? prev.filter(id => id !== deviceId)
|
||||||
|
: [...prev, deviceId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>选择设备</DialogTitle>
|
<DialogTitle>选择设备</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 my-4">
|
<div className="flex items-center space-x-2 my-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索设备IMEI/备注/微信号"
|
placeholder="搜索设备IMEI/备注/微信号"
|
||||||
|
className="pl-9"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-9"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
@@ -140,45 +150,75 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1">
|
<div className="flex justify-between items-center mb-2">
|
||||||
{loading ? (
|
<div className="text-sm text-gray-500">
|
||||||
<div className="flex justify-center items-center py-8">
|
已选择 {tempSelected.length} 个设备
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
</div>
|
||||||
</div>
|
<div className="flex gap-2">
|
||||||
) : (
|
<Button
|
||||||
<div className="space-y-2">
|
variant="outline"
|
||||||
{filteredDevices.map((device) => (
|
size="sm"
|
||||||
<label
|
onClick={handleSelectAll}
|
||||||
|
disabled={loading || filteredDevices.length === 0}
|
||||||
|
>
|
||||||
|
{tempSelected.length === filteredDevices.length ? "取消全选" : "全选"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="h-[400px] -mx-6 px-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
) : filteredDevices.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500">
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredDevices.map((device) => (
|
||||||
|
<Label
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className="flex items-center space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
|
className="flex items-center justify-between p-4 rounded-lg border hover:bg-gray-50 cursor-pointer"
|
||||||
style={{paddingLeft: '0px',paddingRight: '0px'}}
|
htmlFor={device.id}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||||
checked={tempSelectedDevices.includes(device.id)}
|
<Checkbox
|
||||||
onCheckedChange={(checked) => handleDeviceToggle(device.id, checked as boolean)}
|
id={device.id}
|
||||||
className="h-5 w-5"
|
checked={tempSelected.includes(device.id)}
|
||||||
/>
|
onCheckedChange={() => handleDeviceToggle(device.id)}
|
||||||
<div className="flex-1">
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="min-w-0 flex-1">
|
||||||
<span className="font-medium">{device.name}</span>
|
<div className="font-medium truncate mb-1">{device.memo || device.imei}</div>
|
||||||
<Badge variant={device.status === "online" ? "default" : "secondary"}>
|
<div className="text-sm text-gray-500 truncate mb-1">IMEI: {device.imei}</div>
|
||||||
{device.status === "online" ? "在线" : "离线"}
|
<div className="text-sm text-gray-500 truncate">{device.wechatId ? `微信号: ${device.wechatId}` : ''}</div>
|
||||||
</Badge>
|
{device.usedInPlans > 0 && (
|
||||||
</div>
|
<div className="text-sm text-orange-500 mt-1">
|
||||||
<div className="text-sm text-gray-500 mt-1">
|
已用于 {device.usedInPlans} 个计划
|
||||||
<div>IMEI: {device.imei || '--'}</div>
|
</div>
|
||||||
<div>微信号: {device.wxid || '--'}</div>
|
)}
|
||||||
<div>好友数: {device.totalFriend}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
<Badge variant={device.alive === 1 ? "default" : "secondary"}>
|
||||||
))}
|
{device.alive === 1 ? "在线" : "离线"}
|
||||||
</div>
|
</Badge>
|
||||||
)}
|
</Label>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter className="mt-4 flex gap-4 -mx-6 px-6">
|
<DialogFooter className="mt-4">
|
||||||
<Button className="flex-1" onClick={handleConfirm}>确认</Button>
|
{/* <Button variant="outline" onClick={() => handleDialogOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button> */}
|
||||||
|
<Button onClick={() => {
|
||||||
|
onSelect(tempSelected)
|
||||||
|
onOpenChange(false)
|
||||||
|
}}>
|
||||||
|
确定{tempSelected.length > 0 ? ` (${tempSelected.length})` : ''}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -201,21 +201,6 @@ export default function NewMomentsSyncPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="fixed bottom-0 left-0 right-0 h-16 bg-white border-t flex items-center justify-around px-6">
|
|
||||||
<button className="flex flex-col items-center text-blue-600">
|
|
||||||
<span className="text-sm mt-1">首页</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex flex-col items-center text-gray-400">
|
|
||||||
<span className="text-sm mt-1">场景获客</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex flex-col items-center text-gray-400">
|
|
||||||
<span className="text-sm mt-1">工作台</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex flex-col items-center text-gray-400">
|
|
||||||
<span className="text-sm mt-1">我的</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ interface SyncTask {
|
|||||||
lastSyncTime: string
|
lastSyncTime: string
|
||||||
createTime: string
|
createTime: string
|
||||||
creator: string
|
creator: string
|
||||||
libraries?: string[]
|
config: {
|
||||||
devices?: string[]
|
devices: string[]
|
||||||
|
contentLibraryNames: string[]
|
||||||
|
}
|
||||||
|
creatorName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MomentsSyncPage() {
|
export default function MomentsSyncPage() {
|
||||||
@@ -309,19 +312,19 @@ export default function MomentsSyncPage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<div>推送设备:{task.devices?.length || 0} 个</div>
|
<div>推送设备:{task.config.devices.length || 0} 个</div>
|
||||||
<div>内容库:{task.contentLib}</div>
|
<div className="truncate">内容库:{task.config.contentLibraryNames?.join('、') || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<div>已同步:{task.syncCount} 条</div>
|
<div>已同步:{task.syncCount || 0} 条</div>
|
||||||
<div>创建人:{task.creator}</div>
|
<div>创建人:{task.creatorName}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Clock className="w-4 h-4 mr-1" />
|
<Clock className="w-4 h-4 mr-1" />
|
||||||
上次同步:{task.lastSyncTime}
|
上次同步:{task.lastSyncTime || '--'}
|
||||||
</div>
|
</div>
|
||||||
<div>创建时间:{task.createTime}</div>
|
<div>创建时间:{task.createTime}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ Route::group('v1', function () {
|
|||||||
// Device控制器路由
|
// Device控制器路由
|
||||||
Route::group('device', function () {
|
Route::group('device', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\DeviceController@getList'); // 获取设备列表 √
|
Route::get('list', 'app\\api\\controller\\DeviceController@getList'); // 获取设备列表 √
|
||||||
//Route::get('add/:accountId', 'app\\api\\controller\\DeviceController@addDevice'); // 生成设备二维码
|
|
||||||
Route::post('add', 'app\\api\\controller\\DeviceController@addDevice'); // 生成设备二维码(POST方式) √
|
Route::post('add', 'app\\api\\controller\\DeviceController@addDevice'); // 生成设备二维码(POST方式) √
|
||||||
Route::post('updateDeviceGroup', 'app\\api\\controller\\DeviceController@updateDeviceGroup'); // 更新设备分组 √
|
Route::post('updateDeviceGroup', 'app\\api\\controller\\DeviceController@updateDeviceGroup'); // 更新设备分组 √
|
||||||
|
Route::post('updateaccount', 'app\\api\\controller\\DeviceController@updateaccount'); // 更新设备账号 √
|
||||||
Route::post('createGroup', 'app\\api\\controller\\DeviceController@createGroup'); // 创建设备分组 √
|
Route::post('createGroup', 'app\\api\\controller\\DeviceController@createGroup'); // 创建设备分组 √
|
||||||
Route::get('groupList', 'app\\api\\controller\\DeviceController@getGroupList'); // 获取设备分组列表 √
|
Route::get('groupList', 'app\\api\\controller\\DeviceController@getGroupList'); // 获取设备分组列表 √
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -212,6 +212,55 @@ class DeviceController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新设备分组
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function updateaccount()
|
||||||
|
{
|
||||||
|
// 获取授权token
|
||||||
|
$authorization = trim($this->request->header('authorization', $this->authorization));
|
||||||
|
if (empty($authorization)) {
|
||||||
|
return errorJson('缺少授权信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取参数
|
||||||
|
$id = $this->request->param('id', '');
|
||||||
|
$accountId = $this->request->param('accountId', '');
|
||||||
|
|
||||||
|
if (empty($id)) {
|
||||||
|
return errorJson('设备ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($accountId)) {
|
||||||
|
return errorJson('账号id不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
$headerData = ['client:system'];
|
||||||
|
$header = setHeader($headerData, $authorization, 'plain');
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
$result = requestCurl($this->baseUrl . 'api/device/updateaccount?accountId=' . $accountId . '&deviceId=' . $id, [], 'PUT', $header);
|
||||||
|
$response = handleApiResponse($result);
|
||||||
|
|
||||||
|
|
||||||
|
if(empty($response)){
|
||||||
|
return successJson([],'操作成功');
|
||||||
|
}else{
|
||||||
|
return errorJson([],$response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return errorJson('更新设备分组失败:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取设备分组列表
|
* 获取设备分组列表
|
||||||
* @return \think\response\Json
|
* @return \think\response\Json
|
||||||
|
|||||||
@@ -3,17 +3,22 @@
|
|||||||
namespace app\api\controller;
|
namespace app\api\controller;
|
||||||
|
|
||||||
use app\api\model\WechatAccountModel;
|
use app\api\model\WechatAccountModel;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信账号管理控制器
|
||||||
|
*/
|
||||||
class WechatController extends BaseController
|
class WechatController extends BaseController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 获取微信账号列表
|
* 获取微信账号列表(主方法)
|
||||||
|
*
|
||||||
* @param string $pageIndex 页码
|
* @param string $pageIndex 页码
|
||||||
* @param string $pageSize 每页大小
|
* @param string $pageSize 每页大小
|
||||||
* @param bool $isJob 是否为任务调用
|
* @param bool $isJob 是否为任务调用
|
||||||
* @return \think\response\Json
|
* @return \think\response\Json
|
||||||
*/
|
*/
|
||||||
public function getlist($pageIndex = '', $pageSize = '', $isJob = false)
|
public function getList($pageIndex = '', $pageSize = '', $isJob = false)
|
||||||
{
|
{
|
||||||
// 获取授权token
|
// 获取授权token
|
||||||
$authorization = trim($this->request->header('authorization', $this->authorization));
|
$authorization = trim($this->request->header('authorization', $this->authorization));
|
||||||
@@ -43,17 +48,20 @@ class WechatController extends BaseController
|
|||||||
$headerData = ['client:system'];
|
$headerData = ['client:system'];
|
||||||
$header = setHeader($headerData, $authorization, 'plain');
|
$header = setHeader($headerData, $authorization, 'plain');
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求获取基本信息
|
||||||
$result = requestCurl($this->baseUrl . 'api/WechatAccount/list', $params, 'GET', $header);
|
$result = requestCurl($this->baseUrl . 'api/WechatAccount/list', $params, 'GET', $header);
|
||||||
$response = handleApiResponse($result);
|
$response = handleApiResponse($result);
|
||||||
|
|
||||||
// 保存数据到数据库
|
// 保存基本数据到数据库
|
||||||
if (!empty($response['results'])) {
|
if (!empty($response['results'])) {
|
||||||
foreach ($response['results'] as $item) {
|
foreach ($response['results'] as $item) {
|
||||||
$this->saveWechatAccount($item);
|
$this->saveWechatAccount($item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取并更新微信账号状态信息
|
||||||
|
$this->getListTenantWechatPartial($authorization);
|
||||||
|
|
||||||
if ($isJob) {
|
if ($isJob) {
|
||||||
return json_encode(['code' => 200, 'msg' => '获取微信账号列表成功', 'data' => $response]);
|
return json_encode(['code' => 200, 'msg' => '获取微信账号列表成功', 'data' => $response]);
|
||||||
} else {
|
} else {
|
||||||
@@ -69,54 +77,178 @@ class WechatController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存微信账号数据到数据库
|
* 获取微信账号状态信息
|
||||||
|
*
|
||||||
|
* @param string $authorization 授权token
|
||||||
|
* @return \think\response\Json|void
|
||||||
|
*/
|
||||||
|
public function getListTenantWechatPartial($authorization = '')
|
||||||
|
{
|
||||||
|
// 获取授权token(如果未传入)
|
||||||
|
if (empty($authorization)) {
|
||||||
|
$authorization = trim($this->request->header('authorization', $this->authorization));
|
||||||
|
if (empty($authorization)) {
|
||||||
|
return errorJson('缺少授权信息');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从数据库获取微信账号和设备信息
|
||||||
|
$wechatList = Db::table('s2_wechat_account')
|
||||||
|
->where('isDeleted', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
if (empty($wechatList)) {
|
||||||
|
if (empty($authorization)) { // 只有作为独立API调用时才返回
|
||||||
|
return json(['code' => 200, 'msg' => '获取成功', 'data' => []]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造请求参数
|
||||||
|
$wechatAccountIds = [];
|
||||||
|
$deviceIds = [];
|
||||||
|
$accountIds = [];
|
||||||
|
|
||||||
|
foreach ($wechatList as $item) {
|
||||||
|
$wechatAccountIds[] = $item['id'];
|
||||||
|
$deviceIds[] = $item['currentDeviceId'] ?: 0;
|
||||||
|
$accountIds[] = $item['deviceAccountId'] ?: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
$headerData = ['client:system'];
|
||||||
|
$header = setHeader($headerData, $authorization, 'plain');
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'wechatAccountIdsStr' => json_encode($wechatAccountIds),
|
||||||
|
'deviceIdsStr' => json_encode($deviceIds),
|
||||||
|
'accountIdsStr' => json_encode($accountIds),
|
||||||
|
'groupId' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
// 发送请求获取状态信息
|
||||||
|
$result = requestCurl($this->baseUrl . 'api/WechatAccount/listTenantWechatPartial', $params, 'GET', $header);
|
||||||
|
$response = handleApiResponse($result);
|
||||||
|
|
||||||
|
// 如果请求成功并返回数据,则更新数据库
|
||||||
|
if (!empty($response)) {
|
||||||
|
$this->batchUpdateWechatAccounts($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有作为独立API调用时才返回
|
||||||
|
if (empty($authorization)) {
|
||||||
|
// 返回更新后的数据
|
||||||
|
$updatedWechatList = Db::table('s2_wechat_account')
|
||||||
|
->where('isDeleted', 0)
|
||||||
|
->select();
|
||||||
|
return json(['code' => 200, 'msg' => '获取成功', 'data' => $updatedWechatList]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if (empty($authorization)) { // 只有作为独立API调用时才返回
|
||||||
|
return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新微信账号数据
|
||||||
|
*
|
||||||
|
* @param array $data 接口返回的数据
|
||||||
|
*/
|
||||||
|
private function batchUpdateWechatAccounts($data)
|
||||||
|
{
|
||||||
|
// 更新微信账号信息
|
||||||
|
if (!empty($data['totalFriend'])) {
|
||||||
|
// 遍历所有微信账号ID
|
||||||
|
$wechatIds = array_keys($data['totalFriend']);
|
||||||
|
foreach ($wechatIds as $wechatId) {
|
||||||
|
// 构建更新数据
|
||||||
|
$updateData = [
|
||||||
|
'maleFriend' => $data['maleFriend'][$wechatId] ?? 0,
|
||||||
|
'femaleFriend' => $data['femaleFriend'][$wechatId] ?? 0,
|
||||||
|
'unknowFriend' => $data['unknowFriend'][$wechatId] ?? 0,
|
||||||
|
'totalFriend' => $data['totalFriend'][$wechatId] ?? 0,
|
||||||
|
'yesterdayMsgCount' => $data['yesterdayMsgCount'][$wechatId] ?? 0,
|
||||||
|
'sevenDayMsgCount' => $data['sevenDayMsgCount'][$wechatId] ?? 0,
|
||||||
|
'thirtyDayMsgCount' => $data['thirtyDayMsgCount'][$wechatId] ?? 0,
|
||||||
|
'wechatAlive' => isset($data['wechatAlive'][$wechatId]) ? (int)$data['wechatAlive'][$wechatId] : 0,
|
||||||
|
'updateTime' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新数据库
|
||||||
|
Db::table('s2_wechat_account')
|
||||||
|
->where('id', $wechatId)
|
||||||
|
->update($updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备状态
|
||||||
|
if (!empty($data['deviceAlive'])) {
|
||||||
|
foreach ($data['deviceAlive'] as $deviceId => $isAlive) {
|
||||||
|
// 更新微信账号的设备状态
|
||||||
|
Db::table('s2_wechat_account')
|
||||||
|
->where('currentDeviceId', $deviceId)
|
||||||
|
->update([
|
||||||
|
'deviceAlive' => (int)$isAlive,
|
||||||
|
'updateTime' => time()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 更新设备表的状态
|
||||||
|
Db::table('s2_device')
|
||||||
|
->where('id', $deviceId)
|
||||||
|
->update([
|
||||||
|
'alive' => (int)$isAlive,
|
||||||
|
'updateTime' => time()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存微信账号基本数据到数据库
|
||||||
|
*
|
||||||
* @param array $item 微信账号数据
|
* @param array $item 微信账号数据
|
||||||
*/
|
*/
|
||||||
private function saveWechatAccount($item)
|
private function saveWechatAccount($item)
|
||||||
{
|
{
|
||||||
|
// 处理时间字段
|
||||||
$createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0;
|
$createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0;
|
||||||
$deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0;
|
$deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0;
|
||||||
|
|
||||||
|
// 构建数据
|
||||||
$data = [
|
$data = [
|
||||||
'id' => $item['id'],
|
'id' => $item['id'],
|
||||||
'wechatId' => $item['wechatId'],
|
'wechatId' => $item['wechatId'] ?? '',
|
||||||
'deviceAccountId' => $item['deviceAccountId'],
|
'deviceAccountId' => $item['deviceAccountId'] ?? 0,
|
||||||
'imei' => $item['imei'],
|
'imei' => $item['imei'] ?? '',
|
||||||
'deviceMemo' => $item['deviceMemo'],
|
'deviceMemo' => $item['deviceMemo'] ?? '',
|
||||||
'accountUserName' => $item['accountUserName'],
|
'accountUserName' => $item['accountUserName'] ?? '',
|
||||||
'accountRealName' => $item['accountRealName'],
|
'accountRealName' => $item['accountRealName'] ?? '',
|
||||||
'accountNickname' => $item['accountNickname'],
|
'accountNickname' => $item['accountNickname'] ?? '',
|
||||||
'keFuAlive' => $item['keFuAlive'],
|
'wechatGroupName' => $item['wechatGroupName'] ?? '',
|
||||||
'deviceAlive' => $item['deviceAlive'],
|
'alias' => $item['alias'] ?? '',
|
||||||
'wechatAlive' => $item['wechatAlive'],
|
'tenantId' => $item['tenantId'] ?? 0,
|
||||||
'yesterdayMsgCount' => $item['yesterdayMsgCount'],
|
'nickname' => $item['nickname'] ?? '',
|
||||||
'sevenDayMsgCount' => $item['sevenDayMsgCount'],
|
'avatar' => $item['avatar'] ?? '',
|
||||||
'thirtyDayMsgCount' => $item['thirtyDayMsgCount'],
|
'gender' => $item['gender'] ?? 0,
|
||||||
'totalFriend' => $item['totalFriend'],
|
'region' => $item['region'] ?? '',
|
||||||
'maleFriend' => $item['maleFriend'],
|
'signature' => $item['signature'] ?? '',
|
||||||
'femaleFriend' => $item['femaleFriend'],
|
'bindQQ' => $item['bindQQ'] ?? '',
|
||||||
'wechatGroupName' => $item['wechatGroupName'],
|
'bindEmail' => $item['bindEmail'] ?? '',
|
||||||
'tenantId' => $item['tenantId'],
|
'bindMobile' => $item['bindMobile'] ?? '',
|
||||||
'nickname' => $item['nickname'],
|
'currentDeviceId' => $item['currentDeviceId'] ?? 0,
|
||||||
'alias' => $item['alias'],
|
'isDeleted' => $item['isDeleted'] ?? 0,
|
||||||
'avatar' => $item['avatar'],
|
'groupId' => $item['groupId'] ?? 0,
|
||||||
'gender' => $item['gender'],
|
'memo' => $item['memo'] ?? '',
|
||||||
'region' => $item['region'],
|
'wechatVersion' => $item['wechatVersion'] ?? '',
|
||||||
'signature' => $item['signature'],
|
|
||||||
'bindQQ' => $item['bindQQ'],
|
|
||||||
'bindEmail' => $item['bindEmail'],
|
|
||||||
'bindMobile' => $item['bindMobile'],
|
|
||||||
'currentDeviceId' => $item['currentDeviceId'],
|
|
||||||
'isDeleted' => $item['isDeleted'],
|
|
||||||
'groupId' => $item['groupId'],
|
|
||||||
'memo' => $item['memo'],
|
|
||||||
'wechatVersion' => $item['wechatVersion'],
|
|
||||||
'labels' => !empty($item['labels']) ? json_encode($item['labels']) : json_encode([]),
|
'labels' => !empty($item['labels']) ? json_encode($item['labels']) : json_encode([]),
|
||||||
'createTime' => $createTime,
|
'createTime' => $createTime,
|
||||||
'deleteTime' => $deleteTime,
|
'deleteTime' => $deleteTime,
|
||||||
'updateTime' => time()
|
'updateTime' => time()
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 保存或更新数据
|
||||||
$account = WechatAccountModel::where('id', $item['id'])->find();
|
$account = WechatAccountModel::where('id', $item['id'])->find();
|
||||||
if ($account) {
|
if ($account) {
|
||||||
$account->save($data);
|
$account->save($data);
|
||||||
|
|||||||
@@ -168,9 +168,8 @@ class AuthService
|
|||||||
|
|
||||||
// 尝试从缓存获取授权信息
|
// 尝试从缓存获取授权信息
|
||||||
$authorization = Cache::get($cacheKey);
|
$authorization = Cache::get($cacheKey);
|
||||||
$authorization = 'xwz8Uh2doczeTCqSvakElfZMPn-jRce5WTnKTz3ljqpa63PnUOy5beT3TDhxnGNsROofYzpUphfhraxPrQfXvSuMyxFj_vrMUenzptj6hdG8Y4h1NrPXHFUr5Rlw-cIq0uyZZhjYp6xDTLg-IipgyAvBPdJM0vIgbizbo-agd8_Ubwbl0EOPqrMscYdsGrnv9_Lbr_B4-tHMNMa6yerb6kP6rzx8KQ4mJ6Cr5OmPX2WAmFkYykS3p0erWtb9PGHcxgaI1SVkEF4vH2H_iSOxfz5v27xd4HFE63IA5ZtDHQBNeiR0avST36UJSTZz3vjta9FDsw';
|
$authorization = 'aXRi4R80zwTXo9V-VCXVYk4IMLl5ufKASoRtYHfaRh_uLwil_mO9U_jWfxeR1yupJIPuQCZknXGpctr9PTS1hbormw3RSrOwunNKTsvvcGzjTa0bBUz3S9W8x_PtvbY4_JpoXl8x8hm8cUa37zLlN7DQBAmj8He40FCxMTh1MC4xorM11aXoVFvYcrAkv_urHINWDmfNhH9icXzreiX9Uynw4fq7BkuP7yr6WHQ5z0NkOfKoMcesH4gPn_h_OLHC0T_ps2ky--M5HOvd5WgBmYRecNOcqbe4e0oIIO5ffANLsybyhLOEha3a03qKsyfAFWdf0A';
|
||||||
|
// 如果缓存中没有或已过期,则重新获取
|
||||||
// 如果缓存中没有或已过期,则重新获取
|
|
||||||
if (empty($authorization)) {
|
if (empty($authorization)) {
|
||||||
try {
|
try {
|
||||||
// 从环境变量中获取API用户名和密码
|
// 从环境变量中获取API用户名和密码
|
||||||
|
|||||||
@@ -162,19 +162,13 @@ class WorkbenchController extends Controller
|
|||||||
$query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
|
$query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
|
||||||
},
|
},
|
||||||
'user' => function($query) {
|
'user' => function($query) {
|
||||||
$query->field('username');
|
$query->field('id,username');
|
||||||
},
|
}
|
||||||
// 'groupPush' => function($query) {
|
|
||||||
// $query->field('workbenchId,pushInterval,pushContent,pushTime,devices,targetGroups');
|
|
||||||
// },
|
|
||||||
// 'groupCreate' => function($query) {
|
|
||||||
// $query->field('workbenchId,groupNamePrefix,maxGroups,membersPerGroup,devices,targetGroups');
|
|
||||||
// }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$list = Workbench::where($where)
|
$list = Workbench::where($where)
|
||||||
->with($with)
|
->with($with)
|
||||||
->field('id,name,type,status,autoStart,createTime,updateTime')
|
->field('id,name,type,status,autoStart,userId,createTime,updateTime')
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
->page($page, $limit)
|
->page($page, $limit)
|
||||||
->select()
|
->select()
|
||||||
@@ -195,6 +189,15 @@ class WorkbenchController extends Controller
|
|||||||
$item->config = $item->momentsSync;
|
$item->config = $item->momentsSync;
|
||||||
$item->config->devices = json_decode($item->config->devices, true);
|
$item->config->devices = json_decode($item->config->devices, true);
|
||||||
$item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
|
$item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
|
||||||
|
|
||||||
|
// 获取内容库名称
|
||||||
|
if (!empty($item->config->contentLibraries)) {
|
||||||
|
$libraryNames = \app\cunkebao\model\ContentLibrary::where('id', 'in', $item->config->contentLibraries)
|
||||||
|
->column('name');
|
||||||
|
$item->config->contentLibraryNames = $libraryNames;
|
||||||
|
} else {
|
||||||
|
$item->config->contentLibraryNames = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
unset($item->momentsSync,$item->moments_sync);
|
unset($item->momentsSync,$item->moments_sync);
|
||||||
break;
|
break;
|
||||||
@@ -217,6 +220,9 @@ class WorkbenchController extends Controller
|
|||||||
unset($item->groupCreate,$item->group_create);
|
unset($item->groupCreate,$item->group_create);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// 添加创建人名称
|
||||||
|
$item['creatorName'] = $item->user ? $item->user->username : '';
|
||||||
|
unset($item['user']); // 移除关联数据
|
||||||
return $item;
|
return $item;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -292,6 +298,7 @@ class WorkbenchController extends Controller
|
|||||||
$workbench->config = $workbench->momentsSync;
|
$workbench->config = $workbench->momentsSync;
|
||||||
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
||||||
$workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
|
$workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
|
||||||
|
unset($workbench->momentsSync,$workbench->moments_sync);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case self::TYPE_GROUP_PUSH:
|
case self::TYPE_GROUP_PUSH:
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ class User extends Model
|
|||||||
// 定义关联的工作台
|
// 定义关联的工作台
|
||||||
public function workbench()
|
public function workbench()
|
||||||
{
|
{
|
||||||
return $this->hasMany('Workbench', 'id', 'userId');
|
return $this->belongsTo('Workbench', 'id', 'userId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,9 +56,11 @@ class Workbench extends Model
|
|||||||
return $this->hasOne('WorkbenchGroupCreate', 'workbenchId', 'id');
|
return $this->hasOne('WorkbenchGroupCreate', 'workbenchId', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户关联
|
/**
|
||||||
|
* 用户关联
|
||||||
|
*/
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->hasOne('User', 'id', 'userId');
|
return $this->belongsTo('User', 'userId', 'id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ use think\Model;
|
|||||||
*/
|
*/
|
||||||
class WorkbenchAutoLike extends Model
|
class WorkbenchAutoLike extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'ck_workbench_auto_like';
|
|
||||||
protected $pk = 'id';
|
protected $pk = 'id';
|
||||||
protected $name = 'workbench_auto_like';
|
protected $name = 'workbench_auto_like';
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,14 @@ class BaseController extends Api
|
|||||||
if (!$device) {
|
if (!$device) {
|
||||||
$device = Db::name('device_user')
|
$device = Db::name('device_user')
|
||||||
->alias('du')
|
->alias('du')
|
||||||
->join(['s2_device' => 'd'], 'd.id = du.deviceId','left')
|
->join('device d', 'd.id = du.deviceId','left')
|
||||||
->join(['s2_wechat_account' => 'wa'], 'd.id = wa.currentDeviceId','left')
|
->join('device_wechat_login dwl', 'dwl.deviceId = du.deviceId','left')
|
||||||
|
->join('wechat_account wa', 'dwl.wechatId = wa.wechatId','left')
|
||||||
->where([
|
->where([
|
||||||
'du.userId' => $this->userInfo['id'],
|
'du.userId' => $this->userInfo['id'],
|
||||||
'du.companyId' => $this->userInfo['companyId']
|
'du.companyId' => $this->userInfo['companyId']
|
||||||
])
|
])
|
||||||
->field('d.*,wa.id as wechatAccountId,wa.wechatId,wa.alias')
|
->field('d.*,wa.wechatId,wa.alias,wa.s2_wechatAccountId as wechatAccountId')
|
||||||
->find();
|
->find();
|
||||||
// 将设备信息存入缓存
|
// 将设备信息存入缓存
|
||||||
if ($device) {
|
if ($device) {
|
||||||
|
|||||||
@@ -28,25 +28,25 @@ class StatisticsController extends BaseController
|
|||||||
$lastEndTime = $timeRange['last_end_time'];
|
$lastEndTime = $timeRange['last_end_time'];
|
||||||
|
|
||||||
// 1. 总客户数
|
// 1. 总客户数
|
||||||
$totalCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$totalCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereTime('createTime', '>=', $startTime)
|
->whereTime('createTime', '>=', $startTime)
|
||||||
->whereTime('createTime', '<', $endTime)
|
->whereTime('createTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 上期总客户数
|
// 上期总客户数
|
||||||
$lastTotalCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$lastTotalCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereTime('createTime', '>=', $lastStartTime)
|
->whereTime('createTime', '>=', $lastStartTime)
|
||||||
->whereTime('createTime', '<', $lastEndTime)
|
->whereTime('createTime', '<', $lastEndTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 2. 新增客户数
|
// 2. 新增客户数
|
||||||
$newCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$newCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereTime('createTime', '>=', $startTime)
|
->whereTime('createTime', '>=', $startTime)
|
||||||
->whereTime('createTime', '<', $endTime)
|
->whereTime('createTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 上期新增客户数
|
// 上期新增客户数
|
||||||
$lastNewCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$lastNewCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereTime('createTime', '>=', $lastStartTime)
|
->whereTime('createTime', '>=', $lastStartTime)
|
||||||
->whereTime('createTime', '<', $lastEndTime)
|
->whereTime('createTime', '<', $lastEndTime)
|
||||||
->count();
|
->count();
|
||||||
@@ -106,33 +106,33 @@ class StatisticsController extends BaseController
|
|||||||
$endTime = $timeRange['end_time'];
|
$endTime = $timeRange['end_time'];
|
||||||
|
|
||||||
// 1. 客户增长趋势数据
|
// 1. 客户增长趋势数据
|
||||||
$totalCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$totalCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereTime('createTime', '<', $endTime)
|
->whereTime('createTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
$newCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$newCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereTime('createTime', '>=', $startTime)
|
->whereTime('createTime', '>=', $startTime)
|
||||||
->whereTime('createTime', '<', $endTime)
|
->whereTime('createTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 计算流失客户数(假设超过30天未互动的客户为流失客户)
|
// 计算流失客户数(假设超过30天未互动的客户为流失客户)
|
||||||
$lostCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId,'isDeleted'=> 1])
|
$lostCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])->where('createTime','>',0)
|
||||||
->whereTime('deleteTime', '<', date('Y-m-d', strtotime('-30 days')))
|
->whereTime('deleteTime', '<', date('Y-m-d', strtotime('-30 days')))
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 2. 客户来源分布数据
|
// 2. 客户来源分布数据
|
||||||
// 朋友推荐
|
// 朋友推荐
|
||||||
$friendRecommend = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$friendRecommend = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereIn('addFrom', [17, 1000017])
|
->whereIn('addFrom', [17, 1000017])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 微信搜索
|
// 微信搜索
|
||||||
$wechatSearch = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$wechatSearch = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereIn('addFrom', [3, 15, 1000003, 1000015])
|
->whereIn('addFrom', [3, 15, 1000003, 1000015])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 微信群
|
// 微信群
|
||||||
$wechatGroup = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
|
$wechatGroup = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
|
||||||
->whereIn('addFrom', [14, 1000014])
|
->whereIn('addFrom', [14, 1000014])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use think\Model;
|
|||||||
|
|
||||||
class FlowPackageModel extends Model
|
class FlowPackageModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'ck_flow_package';
|
protected $name = 'flow_package';
|
||||||
|
|
||||||
// 定义字段自动转换
|
// 定义字段自动转换
|
||||||
protected $type = [
|
protected $type = [
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use think\Model;
|
|||||||
class FlowPackageOrderModel extends Model
|
class FlowPackageOrderModel extends Model
|
||||||
{
|
{
|
||||||
// 设置表名
|
// 设置表名
|
||||||
protected $table = 'ck_flow_package_order';
|
protected $name = 'flow_package_order';
|
||||||
|
|
||||||
// 自动写入时间戳
|
// 自动写入时间戳
|
||||||
protected $autoWriteTimestamp = true;
|
protected $autoWriteTimestamp = true;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use think\Model;
|
|||||||
|
|
||||||
class UserFlowPackageModel extends Model
|
class UserFlowPackageModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'ck_user_flow_package';
|
protected $name = 'user_flow_package';
|
||||||
/**
|
/**
|
||||||
* 获取用户当前有效的流量套餐
|
* 获取用户当前有效的流量套餐
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ use think\Model;
|
|||||||
|
|
||||||
class WechatFriendModel extends Model
|
class WechatFriendModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 's2_wechat_friend';
|
protected $name = 'wechat_friend';
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user