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

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
Store_vue/node_modules
Store_vue/unpackage
Server/.specstory/
Store_vue/.specstory/
*.zip
*.cursorindexingignore

View File

@@ -34,6 +34,7 @@ interface ContentLibrary {
avatar: string
}[]
creator: string
creatorName?: string
itemCount: number
lastUpdated: string
enabled: boolean
@@ -74,7 +75,7 @@ export default function ContentLibraryPage() {
if (response.code === 200 && response.data) {
// 转换数据格式以匹配原有UI
const transformedLibraries = response.data.list.map((item) => {
const transformedLibraries = response.data.list.map((item: any) => {
const transformedItem: ContentLibrary = {
id: item.id,
name: item.name,
@@ -83,7 +84,8 @@ export default function ContentLibraryPage() {
...(item.sourceFriends || []).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,
lastUpdated: item.updateTime,
enabled: item.isEnabled === 1,

View File

@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, use } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Search } from "lucide-react"
import { Button } from "@/components/ui/button"
@@ -38,7 +38,8 @@ interface Task {
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 [currentStep, setCurrentStep] = useState(1)
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
@@ -62,7 +63,7 @@ export default function EditAutoLikePage({ params }: { params: { id: string } })
const fetchTaskDetail = async () => {
const loadingToast = showToast("正在加载任务信息...", "loading", true);
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) {
const task = response.data
@@ -110,7 +111,7 @@ export default function EditAutoLikePage({ params }: { params: { id: string } })
const loadingToast = showToast("正在更新任务...", "loading", true);
try {
const response = await api.post<ApiResponse>('/v1/workbench/update', {
id: params.id,
id: resolvedParams.id,
type: 1,
name: formData.taskName,
interval: formData.likeInterval,

View File

@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, use } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Search } from "lucide-react"
import { Button } from "@/components/ui/button"
@@ -12,18 +12,23 @@ import { Input } from "@/components/ui/input"
import { api, ApiResponse } from "@/lib/api"
import { showToast } from "@/lib/toast"
// 定义基本设置表单数据类型与BasicSettings组件的formData类型匹配
interface BasicSettingsFormData {
taskName: string
interface Task {
id: string
name: string
status: number
config: {
startTime: string
endTime: string
syncCount: number
syncInterval: number
accountType: "business" | "personal"
enabled: boolean
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 [currentStep, setCurrentStep] = useState(1)
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
@@ -34,31 +39,30 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
startTime: "06:00",
endTime: "23:59",
syncCount: 5,
syncInterval: 30, // 同步间隔默认30分钟
syncInterval: 30,
accountType: "business" as "business" | "personal",
enabled: true,
selectedDevices: [] as string[],
selectedLibraries: [] as string[],
})
// 获取任务详情
useEffect(() => {
const fetchTaskDetail = async () => {
setIsLoading(true)
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) {
const taskData = response.data
setFormData({
taskName: taskData.name || "",
startTime: taskData.startTime || "06:00",
endTime: taskData.endTime || "23:59",
syncCount: taskData.syncCount || 5,
syncInterval: taskData.syncInterval || 30,
accountType: taskData.syncType === 1 ? "business" : "personal",
enabled: !!taskData.enabled,
selectedDevices: taskData.devices || [],
selectedLibraries: taskData.contentLibraries || [],
startTime: taskData.config.startTime || "06:00",
endTime: taskData.config.endTime || "23:59",
syncCount: taskData.config.syncCount || 5,
syncInterval: taskData.config.syncInterval || 30,
accountType: taskData.config.syncType === 1 ? "business" : "personal",
enabled: !!taskData.status,
selectedDevices: taskData.config.devices || [],
selectedLibraries: taskData.config.contentLibraries || [],
})
} else {
showToast(response.msg || "获取任务详情失败", "error")
@@ -74,17 +78,12 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
}
fetchTaskDetail()
}, [params.id, router])
}, [resolvedParams.id, router])
const handleUpdateFormData = (data: Partial<typeof formData>) => {
setFormData((prev) => ({ ...prev, ...data }))
}
// 专门用于基本设置的更新函数
const handleBasicSettingsUpdate = (data: Partial<BasicSettingsFormData>) => {
setFormData((prev) => ({ ...prev, ...data }))
}
const handleNext = () => {
setCurrentStep((prev) => Math.min(prev + 1, 3))
}
@@ -96,16 +95,16 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
const handleComplete = async () => {
try {
const response = await api.post<ApiResponse>('/v1/workbench/update', {
id: params.id,
type: 2, // 朋友圈同步任务类型为2
id: resolvedParams.id,
type: 2,
name: formData.taskName,
syncInterval: formData.syncInterval,
syncCount: formData.syncCount,
syncType: formData.accountType === "business" ? 1 : 2, // 业务号为1人设号为2
syncType: formData.accountType === "business" ? 1 : 2,
startTime: formData.startTime,
endTime: formData.endTime,
accountType: formData.accountType === "business" ? 1 : 2,
status: formData.enabled ? 1 : 0, // 状态0=禁用1=启用
status: formData.enabled ? 1 : 0,
devices: formData.selectedDevices,
contentLibraries: formData.selectedLibraries
});
@@ -150,16 +149,8 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
<div className="mt-8">
{currentStep === 1 && (
<BasicSettings
formData={{
taskName: formData.taskName,
startTime: formData.startTime,
endTime: formData.endTime,
syncCount: formData.syncCount,
syncInterval: formData.syncInterval,
accountType: formData.accountType,
enabled: formData.enabled
}}
onChange={handleBasicSettingsUpdate}
formData={formData}
onChange={handleUpdateFormData}
onNext={handleNext}
/>
)}
@@ -249,21 +240,6 @@ export default function EditMomentsSyncPage({ params }: { params: { id: string }
)}
</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>
)
}

View File

@@ -6,6 +6,7 @@ import { Switch } from "@/components/ui/switch"
import { Plus, Minus, Clock, HelpCircle } from "lucide-react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { useViewMode } from "@/app/components/LayoutWrapper"
import { Label } from "@/components/ui/label"
interface BasicSettingsProps {
formData: {
@@ -24,163 +25,142 @@ interface BasicSettingsProps {
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
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 (
<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>
<div className="text-base font-medium mb-2"></div>
<Label htmlFor="taskName" className="text-base"></Label>
<Input
id="taskName"
value={formData.taskName}
onChange={(e) => onChange({ taskName: e.target.value })}
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 className="text-base font-medium mb-2"></div>
<div className="flex items-center space-x-4">
<div className="relative flex-1">
<Label className="text-base"></Label>
<div className="flex items-center space-x-4 mt-1.5">
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-xl"
onClick={() => handleSyncCountChange(-1)}
>
<Minus className="h-4 w-4" />
</Button>
<div className="w-20 text-center text-base">{formData.syncCount}</div>
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-xl"
onClick={() => handleSyncCountChange(1)}
>
<Plus className="h-4 w-4" />
</Button>
<span className="text-gray-500">/</span>
</div>
</div>
<div>
<Label className="text-base"></Label>
<div className="flex items-center space-x-4 mt-1.5">
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-xl"
onClick={() => handleSyncIntervalChange(-5)}
>
<Minus className="h-4 w-4" />
</Button>
<div className="w-20 text-center text-base">{formData.syncInterval}</div>
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-xl"
onClick={() => handleSyncIntervalChange(5)}
>
<Plus className="h-4 w-4" />
</Button>
<span className="text-gray-500"></span>
</div>
<p className="text-sm text-gray-500 mt-1.5"></p>
</div>
<div>
<Label className="text-base"></Label>
<div className="grid grid-cols-2 gap-4 mt-1.5">
<Input
type="time"
value={formData.startTime}
onChange={(e) => onChange({ startTime: e.target.value })}
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
className="h-12 rounded-xl"
/>
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
</div>
<span className="text-gray-500"></span>
<div className="relative flex-1">
<Input
type="time"
value={formData.endTime}
onChange={(e) => onChange({ endTime: e.target.value })}
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
className="h-12 rounded-xl"
/>
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
</div>
</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({ syncCount: Math.max(1, formData.syncCount - 1) })}
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.syncCount}</span>
<Button
variant="outline"
size="lg"
onClick={() => onChange({ syncCount: formData.syncCount + 1 })}
className="h-12 w-12 rounded-xl bg-white border-gray-200"
>
<Plus className="h-5 w-5" />
</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>
<span className="text-gray-500"></span>
</div>
</div>
<div>
<div className="text-base font-medium mb-2"></div>
<div className="flex space-x-4">
<div className="flex-1 relative">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
onClick={() => onChange({ accountType: "business" })}
className={`w-full h-12 justify-between rounded-lg ${
<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"
? "bg-blue-600 hover:bg-blue-600 text-white"
: "bg-white hover:bg-gray-50"
? "border-blue-500 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-600"
}`}
onClick={() => onChange({ accountType: "business" })}
>
<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 ${
</button>
<button
type="button"
className={`h-12 rounded-xl border ${
formData.accountType === "personal"
? "bg-blue-600 hover:bg-blue-600 text-white"
: "bg-white hover:bg-gray-50"
? "border-blue-500 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-600"
}`}
onClick={() => onChange({ accountType: "personal" })}
>
<HelpCircle
className={`h-4 w-4 ${formData.accountType === "personal" ? "text-white/70" : "text-gray-400"}`}
/>
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</button>
</div>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-base font-medium"></span>
<div className="flex items-center justify-between">
<Label className="text-base"></Label>
<Switch
checked={formData.enabled}
onCheckedChange={(checked) => onChange({ enabled: checked })}
className="data-[state=checked]:bg-blue-600 h-7 w-12"
/>
</div>
</div>
<Button
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>

View File

@@ -1,11 +1,26 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useState, useEffect } from "react"
import { Search, RefreshCw, Loader2 } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Search, CheckCircle2, Circle } from "lucide-react"
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 {
open: boolean
@@ -21,93 +36,162 @@ export function ContentLibrarySelectionDialog({
onSelect,
}: ContentLibrarySelectionDialogProps) {
const [searchQuery, setSearchQuery] = useState("")
const [libraries] = useState([
{ id: "1", name: "卡若朋友圈", count: 58 },
{ id: "2", name: "暗黑4代练", count: 422 },
{ id: "3", name: "家装设计", count: 107 },
{ id: "4", name: "美食分享", count: 321 },
{ id: "5", name: "旅游攻略", count: 89 },
])
const [loading, setLoading] = useState(false)
const [libraries, setLibraries] = useState<ContentLibrary[]>([])
const [tempSelected, setTempSelected] = useState<string[]>([])
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) => {
setTempSelectedLibraries((prev) =>
prev.includes(libraryId) ? prev.filter((id) => id !== libraryId) : [...prev, libraryId]
if (response.code === 200 && response.data) {
setLibraries(response.data.list)
} 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 = () => {
onSelect(tempSelectedLibraries)
onOpenChange(false)
const handleDialogOpenChange = (open: boolean) => {
if (!open) {
setTempSelected(selectedLibraries)
}
const handleCancel = () => {
setTempSelectedLibraries(selectedLibraries)
onOpenChange(false)
onOpenChange(open)
}
const filteredLibraries = libraries.filter((library) =>
library.name.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md p-0 overflow-hidden">
<DialogHeader className="px-4 py-3 border-b">
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="p-4">
<div className="relative mb-4">
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
<div className="flex items-center space-x-2 my-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索内容库"
className="pl-10"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<ScrollArea className="h-[300px] pr-4">
<div className="space-y-2">
{filteredLibraries.map((library) => (
<div
key={library.id}
className="flex items-center justify-between p-3 rounded-lg border hover:bg-gray-50 cursor-pointer"
onClick={() => toggleLibrary(library.id)}
>
<div>
<h3 className="font-medium">{library.name}</h3>
<p className="text-sm text-gray-500">{library.count}</p>
</div>
{tempSelectedLibraries.includes(library.id) ? (
<CheckCircle2 className="h-6 w-6 text-blue-500" />
) : (
<Circle className="h-6 w-6 text-gray-300" />
)}
</div>
))}
</div>
</ScrollArea>
<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>
<div className="flex border-t p-4">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-gray-500">
{tempSelected.length}
</div>
<div className="flex gap-2">
<Button
variant="outline"
className="flex-1 mr-2"
onClick={handleCancel}
size="sm"
onClick={handleSelectAll}
disabled={loading || libraries.length === 0}
>
</Button>
<Button
className="flex-1"
onClick={handleConfirm}
disabled={tempSelectedLibraries.length === 0}
>
({tempSelectedLibraries.length})
{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}
className="flex items-center justify-between p-4 rounded-lg border hover:bg-gray-50 cursor-pointer"
htmlFor={library.id}
>
<div className="flex items-center space-x-3 flex-1 min-w-0 pr-4">
<Checkbox
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>
{/* <Badge variant={library.status === 1 ? "default" : "secondary"}>
{library.status === 1 ? "启用" : "已停用"}
</Badge> */}
</Label>
))
)}
</div>
</ScrollArea>
<DialogFooter className="mt-4">
{/* <Button variant="outline" onClick={() => handleDialogOpenChange(false)}>
取消
</Button> */}
<Button onClick={() => {
onSelect(tempSelected)
onOpenChange(false)
}}>
{tempSelected.length > 0 ? ` (${tempSelected.length})` : ''}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)

View File

@@ -1,58 +1,51 @@
"use client"
import { useState, useEffect } from "react"
import { Search, RefreshCw, Loader2 } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
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 { 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 ServerDevice {
id: number
imei: string
memo: string
wechatId: string
alive: number
totalFriend: number
}
interface Device {
id: number
id: string
name: string
imei: string
wxid: string
status: "online" | "offline"
totalFriend: number
wechatId: string
memo?: string
alive: number
usedInPlans: number
lastActiveTime: string
}
interface DeviceSelectionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
selectedDevices: number[]
onSelect: (devices: number[]) => void
selectedDevices: string[]
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 [statusFilter, setStatusFilter] = useState("all")
const [devices, setDevices] = useState<Device[]>([])
const [loading, setLoading] = useState(false)
const [tempSelectedDevices, setTempSelectedDevices] = useState<number[]>(selectedDevices)
useEffect(() => {
if (open) {
setTempSelectedDevices(selectedDevices)
fetchDevices()
}
}, [open, selectedDevices])
const [devices, setDevices] = useState<Device[]>([])
const [tempSelected, setTempSelected] = useState<string[]>([])
// 获取设备列表
const fetchDevices = async () => {
const loadingToast = showToast("正在加载设备列表...", "loading", true);
setLoading(true)
try {
setLoading(true)
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")
}
} catch (error: any) {
console.error('获取设备列表失败:', error)
console.error("获取设备列表失败:", error)
showToast(error?.message || "请检查网络连接", "error")
} finally {
loadingToast.remove();
setLoading(false)
}
}
useEffect(() => {
if (open) {
fetchDevices()
setTempSelected(selectedDevices)
}
}, [open, searchQuery, selectedDevices])
const handleRefresh = () => {
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 matchesSearch = searchQuery === "" ||
const matchesSearch = !searchQuery ||
device.name.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
})
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</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">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索设备IMEI/备注/微信号"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
@@ -140,45 +150,75 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
</Button>
</div>
<ScrollArea className="flex-1">
<div className="flex justify-between items-center mb-2">
<div className="text-sm text-gray-500">
{tempSelected.length}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
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 justify-center items-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<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>
) : (
<div className="space-y-2">
{filteredDevices.map((device) => (
<label
filteredDevices.map((device) => (
<Label
key={device.id}
className="flex items-center space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
style={{paddingLeft: '0px',paddingRight: '0px'}}
className="flex items-center justify-between p-4 rounded-lg border hover:bg-gray-50 cursor-pointer"
htmlFor={device.id}
>
<div className="flex items-center space-x-3 flex-1 min-w-0">
<Checkbox
checked={tempSelectedDevices.includes(device.id)}
onCheckedChange={(checked) => handleDeviceToggle(device.id, checked as boolean)}
className="h-5 w-5"
id={device.id}
checked={tempSelected.includes(device.id)}
onCheckedChange={() => handleDeviceToggle(device.id)}
/>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium">{device.name}</span>
<Badge variant={device.status === "online" ? "default" : "secondary"}>
{device.status === "online" ? "在线" : "离线"}
</Badge>
</div>
<div className="text-sm text-gray-500 mt-1">
<div>IMEI: {device.imei || '--'}</div>
<div>: {device.wxid || '--'}</div>
<div>: {device.totalFriend}</div>
</div>
</div>
</label>
))}
<div className="min-w-0 flex-1">
<div className="font-medium truncate mb-1">{device.memo || device.imei}</div>
<div className="text-sm text-gray-500 truncate mb-1">IMEI: {device.imei}</div>
<div className="text-sm text-gray-500 truncate">{device.wechatId ? `微信号: ${device.wechatId}` : ''}</div>
{device.usedInPlans > 0 && (
<div className="text-sm text-orange-500 mt-1">
{device.usedInPlans}
</div>
)}
</div>
</div>
<Badge variant={device.alive === 1 ? "default" : "secondary"}>
{device.alive === 1 ? "在线" : "离线"}
</Badge>
</Label>
))
)}
</div>
</ScrollArea>
<DialogFooter className="mt-4 flex gap-4 -mx-6 px-6">
<Button className="flex-1" onClick={handleConfirm}></Button>
<DialogFooter className="mt-4">
{/* <Button variant="outline" onClick={() => handleDialogOpenChange(false)}>
取消
</Button> */}
<Button onClick={() => {
onSelect(tempSelected)
onOpenChange(false)
}}>
{tempSelected.length > 0 ? ` (${tempSelected.length})` : ''}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -201,21 +201,6 @@ export default function NewMomentsSyncPage() {
)}
</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>
)
}

View File

@@ -34,8 +34,11 @@ interface SyncTask {
lastSyncTime: string
createTime: string
creator: string
libraries?: string[]
devices?: string[]
config: {
devices: string[]
contentLibraryNames: string[]
}
creatorName: string
}
export default function MomentsSyncPage() {
@@ -309,19 +312,19 @@ export default function MomentsSyncPage() {
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="text-sm text-gray-500">
<div>{task.devices?.length || 0} </div>
<div>{task.contentLib}</div>
<div>{task.config.devices.length || 0} </div>
<div className="truncate">{task.config.contentLibraryNames?.join('、') || '--'}</div>
</div>
<div className="text-sm text-gray-500">
<div>{task.syncCount} </div>
<div>{task.creator}</div>
<div>{task.syncCount || 0} </div>
<div>{task.creatorName}</div>
</div>
</div>
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{task.lastSyncTime}
{task.lastSyncTime || '--'}
</div>
<div>{task.createTime}</div>
</div>

View File

@@ -20,9 +20,9 @@ Route::group('v1', function () {
// Device控制器路由
Route::group('device', function () {
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('updateDeviceGroup', 'app\\api\\controller\\DeviceController@updateDeviceGroup'); // 更新设备分组 √
Route::post('updateaccount', 'app\\api\\controller\\DeviceController@updateaccount'); // 更新设备账号 √
Route::post('createGroup', 'app\\api\\controller\\DeviceController@createGroup'); // 创建设备分组 √
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

View File

@@ -3,17 +3,22 @@
namespace app\api\controller;
use app\api\model\WechatAccountModel;
use think\Db;
/**
* 微信账号管理控制器
*/
class WechatController extends BaseController
{
/**
* 获取微信账号列表
* 获取微信账号列表(主方法)
*
* @param string $pageIndex 页码
* @param string $pageSize 每页大小
* @param bool $isJob 是否为任务调用
* @return \think\response\Json
*/
public function getlist($pageIndex = '', $pageSize = '', $isJob = false)
public function getList($pageIndex = '', $pageSize = '', $isJob = false)
{
// 获取授权token
$authorization = trim($this->request->header('authorization', $this->authorization));
@@ -43,17 +48,20 @@ class WechatController extends BaseController
$headerData = ['client:system'];
$header = setHeader($headerData, $authorization, 'plain');
// 发送请求
// 发送请求获取基本信息
$result = requestCurl($this->baseUrl . 'api/WechatAccount/list', $params, 'GET', $header);
$response = handleApiResponse($result);
// 保存数据到数据库
// 保存基本数据到数据库
if (!empty($response['results'])) {
foreach ($response['results'] as $item) {
$this->saveWechatAccount($item);
}
}
// 获取并更新微信账号状态信息
$this->getListTenantWechatPartial($authorization);
if ($isJob) {
return json_encode(['code' => 200, 'msg' => '获取微信账号列表成功', 'data' => $response]);
} 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 微信账号数据
*/
private function saveWechatAccount($item)
{
// 处理时间字段
$createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0;
$deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0;
// 构建数据
$data = [
'id' => $item['id'],
'wechatId' => $item['wechatId'],
'deviceAccountId' => $item['deviceAccountId'],
'imei' => $item['imei'],
'deviceMemo' => $item['deviceMemo'],
'accountUserName' => $item['accountUserName'],
'accountRealName' => $item['accountRealName'],
'accountNickname' => $item['accountNickname'],
'keFuAlive' => $item['keFuAlive'],
'deviceAlive' => $item['deviceAlive'],
'wechatAlive' => $item['wechatAlive'],
'yesterdayMsgCount' => $item['yesterdayMsgCount'],
'sevenDayMsgCount' => $item['sevenDayMsgCount'],
'thirtyDayMsgCount' => $item['thirtyDayMsgCount'],
'totalFriend' => $item['totalFriend'],
'maleFriend' => $item['maleFriend'],
'femaleFriend' => $item['femaleFriend'],
'wechatGroupName' => $item['wechatGroupName'],
'tenantId' => $item['tenantId'],
'nickname' => $item['nickname'],
'alias' => $item['alias'],
'avatar' => $item['avatar'],
'gender' => $item['gender'],
'region' => $item['region'],
'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'],
'wechatId' => $item['wechatId'] ?? '',
'deviceAccountId' => $item['deviceAccountId'] ?? 0,
'imei' => $item['imei'] ?? '',
'deviceMemo' => $item['deviceMemo'] ?? '',
'accountUserName' => $item['accountUserName'] ?? '',
'accountRealName' => $item['accountRealName'] ?? '',
'accountNickname' => $item['accountNickname'] ?? '',
'wechatGroupName' => $item['wechatGroupName'] ?? '',
'alias' => $item['alias'] ?? '',
'tenantId' => $item['tenantId'] ?? 0,
'nickname' => $item['nickname'] ?? '',
'avatar' => $item['avatar'] ?? '',
'gender' => $item['gender'] ?? 0,
'region' => $item['region'] ?? '',
'signature' => $item['signature'] ?? '',
'bindQQ' => $item['bindQQ'] ?? '',
'bindEmail' => $item['bindEmail'] ?? '',
'bindMobile' => $item['bindMobile'] ?? '',
'currentDeviceId' => $item['currentDeviceId'] ?? 0,
'isDeleted' => $item['isDeleted'] ?? 0,
'groupId' => $item['groupId'] ?? 0,
'memo' => $item['memo'] ?? '',
'wechatVersion' => $item['wechatVersion'] ?? '',
'labels' => !empty($item['labels']) ? json_encode($item['labels']) : json_encode([]),
'createTime' => $createTime,
'deleteTime' => $deleteTime,
'updateTime' => time()
];
// 保存或更新数据
$account = WechatAccountModel::where('id', $item['id'])->find();
if ($account) {
$account->save($data);

View File

@@ -168,8 +168,7 @@ class AuthService
// 尝试从缓存获取授权信息
$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)) {
try {

View File

@@ -162,19 +162,13 @@ class WorkbenchController extends Controller
$query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
},
'user' => function($query) {
$query->field('username');
},
// 'groupPush' => function($query) {
// $query->field('workbenchId,pushInterval,pushContent,pushTime,devices,targetGroups');
// },
// 'groupCreate' => function($query) {
// $query->field('workbenchId,groupNamePrefix,maxGroups,membersPerGroup,devices,targetGroups');
// }
$query->field('id,username');
}
];
$list = Workbench::where($where)
->with($with)
->field('id,name,type,status,autoStart,createTime,updateTime')
->field('id,name,type,status,autoStart,userId,createTime,updateTime')
->order('id', 'desc')
->page($page, $limit)
->select()
@@ -195,6 +189,15 @@ class WorkbenchController extends Controller
$item->config = $item->momentsSync;
$item->config->devices = json_decode($item->config->devices, true);
$item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
// 获取内容库名称
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);
break;
@@ -217,6 +220,9 @@ class WorkbenchController extends Controller
unset($item->groupCreate,$item->group_create);
break;
}
// 添加创建人名称
$item['creatorName'] = $item->user ? $item->user->username : '';
unset($item['user']); // 移除关联数据
return $item;
});
@@ -292,6 +298,7 @@ class WorkbenchController extends Controller
$workbench->config = $workbench->momentsSync;
$workbench->config->devices = json_decode($workbench->config->devices, true);
$workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
unset($workbench->momentsSync,$workbench->moments_sync);
}
break;
case self::TYPE_GROUP_PUSH:

View File

@@ -20,6 +20,6 @@ class User extends Model
// 定义关联的工作台
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');
}
// 用户关联
/**
* 用户关联
*/
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
{
protected $table = 'ck_workbench_auto_like';
protected $pk = 'id';
protected $name = 'workbench_auto_like';

View File

@@ -37,13 +37,14 @@ class BaseController extends Api
if (!$device) {
$device = Db::name('device_user')
->alias('du')
->join(['s2_device' => 'd'], 'd.id = du.deviceId','left')
->join(['s2_wechat_account' => 'wa'], 'd.id = wa.currentDeviceId','left')
->join('device d', 'd.id = du.deviceId','left')
->join('device_wechat_login dwl', 'dwl.deviceId = du.deviceId','left')
->join('wechat_account wa', 'dwl.wechatId = wa.wechatId','left')
->where([
'du.userId' => $this->userInfo['id'],
'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();
// 将设备信息存入缓存
if ($device) {

View File

@@ -28,25 +28,25 @@ class StatisticsController extends BaseController
$lastEndTime = $timeRange['last_end_time'];
// 1. 总客户数
$totalCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$totalCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereTime('createTime', '>=', $startTime)
->whereTime('createTime', '<', $endTime)
->count();
// 上期总客户数
$lastTotalCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$lastTotalCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereTime('createTime', '>=', $lastStartTime)
->whereTime('createTime', '<', $lastEndTime)
->count();
// 2. 新增客户数
$newCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$newCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereTime('createTime', '>=', $startTime)
->whereTime('createTime', '<', $endTime)
->count();
// 上期新增客户数
$lastNewCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$lastNewCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereTime('createTime', '>=', $lastStartTime)
->whereTime('createTime', '<', $lastEndTime)
->count();
@@ -106,33 +106,33 @@ class StatisticsController extends BaseController
$endTime = $timeRange['end_time'];
// 1. 客户增长趋势数据
$totalCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$totalCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereTime('createTime', '<', $endTime)
->count();
$newCustomers = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$newCustomers = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereTime('createTime', '>=', $startTime)
->whereTime('createTime', '<', $endTime)
->count();
// 计算流失客户数假设超过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')))
->count();
// 2. 客户来源分布数据
// 朋友推荐
$friendRecommend = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$friendRecommend = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereIn('addFrom', [17, 1000017])
->count();
// 微信搜索
$wechatSearch = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$wechatSearch = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereIn('addFrom', [3, 15, 1000003, 1000015])
->count();
// 微信群
$wechatGroup = WechatFriendModel::where(['wechatAccountId'=> $wechatAccountId])
$wechatGroup = WechatFriendModel::where(['ownerWechatId'=> $wechatAccountId])
->whereIn('addFrom', [14, 1000014])
->count();

View File

@@ -6,7 +6,7 @@ use think\Model;
class FlowPackageModel extends Model
{
protected $table = 'ck_flow_package';
protected $name = 'flow_package';
// 定义字段自动转换
protected $type = [

View File

@@ -10,7 +10,7 @@ use think\Model;
class FlowPackageOrderModel extends Model
{
// 设置表名
protected $table = 'ck_flow_package_order';
protected $name = 'flow_package_order';
// 自动写入时间戳
protected $autoWriteTimestamp = true;

View File

@@ -6,7 +6,7 @@ use think\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
{
protected $table = 's2_wechat_friend';
protected $name = 'wechat_friend';
}