门店端适配新表 及操盘手端工作台优化

This commit is contained in:
wong
2025-04-17 09:46:47 +08:00
parent 4f0ea36691
commit dbeb0aa559
23 changed files with 687 additions and 423 deletions

4
.gitignore vendored
View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>
) )
} }

View File

@@ -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>

View File

@@ -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>
) )

View File

@@ -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>

View File

@@ -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>
) )
} }

View File

@@ -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>

View File

@@ -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'); // 获取设备分组列表 √
}); });

View File

@@ -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

View File

@@ -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);

View File

@@ -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用户名和密码

View File

@@ -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:

View File

@@ -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');
} }
} }

View File

@@ -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');
} }
} }

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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 = [

View File

@@ -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;

View File

@@ -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';
/** /**
* 获取用户当前有效的流量套餐 * 获取用户当前有效的流量套餐
* *

View File

@@ -6,6 +6,6 @@ use think\Model;
class WechatFriendModel extends Model class WechatFriendModel extends Model
{ {
protected $table = 's2_wechat_friend'; protected $name = 'wechat_friend';
} }