内容库素材列表 + 编辑
This commit is contained in:
248
Cunkebao/app/content/[id]/materials/edit/[materialId]/page.tsx
Normal file
248
Cunkebao/app/content/[id]/materials/edit/[materialId]/page.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { useState, useEffect, use } from "react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { ChevronLeft, Plus, X, Image as ImageIcon, UploadCloud } from "lucide-react"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { toast } from "@/components/ui/use-toast"
|
||||||
|
import { api } from "@/lib/api"
|
||||||
|
import { showToast } from "@/lib/toast"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
|
interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
msg: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Material {
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
coverImage: string | null
|
||||||
|
resUrls: string[]
|
||||||
|
urls: string[]
|
||||||
|
createTime: string
|
||||||
|
createMomentTime: number
|
||||||
|
time: string
|
||||||
|
wechatId: string
|
||||||
|
friendId: string | null
|
||||||
|
wechatChatroomId: number
|
||||||
|
senderNickname: string
|
||||||
|
location: string | null
|
||||||
|
lat: string
|
||||||
|
lng: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImageUrl = (url: string) => {
|
||||||
|
return /\.(jpg|jpeg|png|gif|webp)$/i.test(url) || url.includes('oss-cn-shenzhen.aliyuncs.com')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditMaterialPage({ params }: { params: Promise<{ id: string, materialId: string }> }) {
|
||||||
|
const resolvedParams = use(params)
|
||||||
|
const router = useRouter()
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [content, setContent] = useState("")
|
||||||
|
const [images, setImages] = useState<string[]>([])
|
||||||
|
const [previewUrls, setPreviewUrls] = useState<string[]>([])
|
||||||
|
const [originalMaterial, setOriginalMaterial] = useState<Material | null>(null)
|
||||||
|
|
||||||
|
// 获取素材详情
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMaterialDetail = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await api.get<ApiResponse<Material>>(`/v1/content/library/get-item-detail?id=${resolvedParams.materialId}`)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
const material = response.data
|
||||||
|
setOriginalMaterial(material)
|
||||||
|
setContent(material.content)
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
const imageUrls: string[] = []
|
||||||
|
|
||||||
|
// 检查内容本身是否为图片链接
|
||||||
|
if (isImageUrl(material.content)) {
|
||||||
|
if (!imageUrls.includes(material.content)) {
|
||||||
|
imageUrls.push(material.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加资源URL中的图片
|
||||||
|
material.resUrls.forEach(url => {
|
||||||
|
if (isImageUrl(url) && !imageUrls.includes(url)) {
|
||||||
|
imageUrls.push(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setImages(imageUrls)
|
||||||
|
setPreviewUrls(imageUrls)
|
||||||
|
} else {
|
||||||
|
showToast(response.msg || "获取素材详情失败", "error")
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to fetch material detail:", error)
|
||||||
|
showToast(error?.message || "请检查网络连接", "error")
|
||||||
|
router.back()
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMaterialDetail()
|
||||||
|
}, [resolvedParams.materialId, router])
|
||||||
|
|
||||||
|
// 模拟上传图片
|
||||||
|
const handleUploadImage = () => {
|
||||||
|
// 这里应该是真实的图片上传逻辑
|
||||||
|
// 为了演示,这里模拟添加一些示例图片URL
|
||||||
|
const mockImageUrls = [
|
||||||
|
"https://picsum.photos/id/237/200/300",
|
||||||
|
"https://picsum.photos/id/238/200/300",
|
||||||
|
"https://picsum.photos/id/239/200/300"
|
||||||
|
]
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * mockImageUrls.length)
|
||||||
|
const newImage = mockImageUrls[randomIndex]
|
||||||
|
|
||||||
|
if (!images.includes(newImage)) {
|
||||||
|
setImages([...images, newImage])
|
||||||
|
setPreviewUrls([...previewUrls, newImage])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveImage = (indexToRemove: number) => {
|
||||||
|
setImages(images.filter((_, index) => index !== indexToRemove))
|
||||||
|
setPreviewUrls(previewUrls.filter((_, index) => index !== indexToRemove))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!content && images.length === 0) {
|
||||||
|
showToast("请输入素材内容或上传图片", "error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingToast = showToast("正在更新素材...", "loading", true)
|
||||||
|
try {
|
||||||
|
const response = await api.post<ApiResponse>('/v1/content/library/update-item', {
|
||||||
|
id: resolvedParams.materialId,
|
||||||
|
content: content,
|
||||||
|
resUrls: images
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
showToast("素材更新成功", "success")
|
||||||
|
router.push(`/content/${resolvedParams.id}/materials`)
|
||||||
|
} else {
|
||||||
|
showToast(response.msg || "更新失败", "error")
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to update material:", error)
|
||||||
|
showToast(error?.message || "更新失败", "error")
|
||||||
|
} finally {
|
||||||
|
loadingToast.remove && loadingToast.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-center">加载中...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||||
|
<header className="sticky top-0 z-10 bg-white border-b">
|
||||||
|
<div className="flex items-center justify-between p-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||||
|
<ChevronLeft className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-lg font-medium">编辑素材</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{/* 只有当内容不是图片链接时才显示内容编辑区 */}
|
||||||
|
{!isImageUrl(content) && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="content">素材内容</Label>
|
||||||
|
<Textarea
|
||||||
|
id="content"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="请输入素材内容"
|
||||||
|
className="mt-1"
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>图片集</Label>
|
||||||
|
<div className="mt-2 border border-dashed border-gray-300 rounded-lg p-4 text-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleUploadImage}
|
||||||
|
className="w-full py-8 flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<UploadCloud className="h-8 w-8 mb-2 text-gray-400" />
|
||||||
|
<span>点击上传图片</span>
|
||||||
|
<span className="text-xs text-gray-500 mt-1">支持 JPG、PNG 格式</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{previewUrls.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<Label>已上传图片</Label>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-2">
|
||||||
|
{previewUrls.map((url, index) => (
|
||||||
|
<div key={index} className="relative group">
|
||||||
|
<div className="aspect-square relative rounded-lg overflow-hidden border border-gray-200">
|
||||||
|
<Image
|
||||||
|
src={url}
|
||||||
|
alt={`图片 ${index + 1}`}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6 p-0"
|
||||||
|
onClick={() => handleRemoveImage(index)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
保存修改
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,38 +4,50 @@ import type React from "react"
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { ChevronLeft, Plus, X } from "lucide-react"
|
import { ChevronLeft, Plus, X, Image as ImageIcon, UploadCloud } from "lucide-react"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
export default function NewMaterialPage({ params }: { params: { id: string } }) {
|
export default function NewMaterialPage({ params }: { params: { id: string } }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [content, setContent] = useState("")
|
const [content, setContent] = useState("")
|
||||||
const [newTag, setNewTag] = useState("")
|
const [images, setImages] = useState<string[]>([])
|
||||||
const [tags, setTags] = useState<string[]>([])
|
const [previewUrls, setPreviewUrls] = useState<string[]>([])
|
||||||
|
|
||||||
const handleAddTag = () => {
|
// 模拟上传图片
|
||||||
if (newTag && !tags.includes(newTag)) {
|
const handleUploadImage = () => {
|
||||||
setTags([...tags, newTag])
|
// 这里应该是真实的图片上传逻辑
|
||||||
setNewTag("")
|
// 为了演示,这里模拟添加一些示例图片URL
|
||||||
|
const mockImageUrls = [
|
||||||
|
"https://picsum.photos/id/237/200/300",
|
||||||
|
"https://picsum.photos/id/238/200/300",
|
||||||
|
"https://picsum.photos/id/239/200/300"
|
||||||
|
]
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * mockImageUrls.length)
|
||||||
|
const newImage = mockImageUrls[randomIndex]
|
||||||
|
|
||||||
|
if (!images.includes(newImage)) {
|
||||||
|
setImages([...images, newImage])
|
||||||
|
setPreviewUrls([...previewUrls, newImage])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveTag = (tagToRemove: string) => {
|
const handleRemoveImage = (indexToRemove: number) => {
|
||||||
setTags(tags.filter((tag) => tag !== tagToRemove))
|
setImages(images.filter((_, index) => index !== indexToRemove))
|
||||||
|
setPreviewUrls(previewUrls.filter((_, index) => index !== indexToRemove))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!content) {
|
if (!content && images.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
title: "错误",
|
title: "错误",
|
||||||
description: "请输入素材内容",
|
description: "请输入素材内容或上传图片",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -87,36 +99,48 @@ export default function NewMaterialPage({ params }: { params: { id: string } })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tags">标签</Label>
|
<Label>图片集</Label>
|
||||||
<div className="flex items-center mt-1">
|
<div className="mt-2 border border-dashed border-gray-300 rounded-lg p-4 text-center">
|
||||||
<Input
|
<Button
|
||||||
id="tags"
|
type="button"
|
||||||
value={newTag}
|
variant="outline"
|
||||||
onChange={(e) => setNewTag(e.target.value)}
|
onClick={handleUploadImage}
|
||||||
placeholder="输入标签"
|
className="w-full py-8 flex flex-col items-center justify-center"
|
||||||
className="flex-1"
|
>
|
||||||
/>
|
<UploadCloud className="h-8 w-8 mb-2 text-gray-400" />
|
||||||
<Button type="button" onClick={handleAddTag} className="ml-2">
|
<span>点击上传图片</span>
|
||||||
<Plus className="h-4 w-4" />
|
<span className="text-xs text-gray-500 mt-1">支持 JPG、PNG 格式</span>
|
||||||
添加
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 mt-2">
|
|
||||||
{tags.map((tag, index) => (
|
{previewUrls.length > 0 && (
|
||||||
<Badge key={index} variant="secondary" className="flex items-center">
|
<div className="mt-4">
|
||||||
{tag}
|
<Label>已上传图片</Label>
|
||||||
<Button
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-2">
|
||||||
type="button"
|
{previewUrls.map((url, index) => (
|
||||||
variant="ghost"
|
<div key={index} className="relative group">
|
||||||
size="sm"
|
<div className="aspect-square relative rounded-lg overflow-hidden border border-gray-200">
|
||||||
className="h-4 w-4 ml-1 p-0"
|
<Image
|
||||||
onClick={() => handleRemoveTag(tag)}
|
src={url}
|
||||||
>
|
alt={`图片 ${index + 1}`}
|
||||||
<X className="h-3 w-3" />
|
fill
|
||||||
</Button>
|
className="object-cover"
|
||||||
</Badge>
|
/>
|
||||||
))}
|
</div>
|
||||||
</div>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6 p-0"
|
||||||
|
onClick={() => handleRemoveImage(index)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
<Button type="submit" className="w-full">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, useCallback, use } from "react"
|
||||||
import { ChevronLeft, Download, Plus, Search, Tag, Trash2, BarChart } from "lucide-react"
|
import { ChevronLeft, Download, Plus, Search, Tag, Trash2, BarChart, RefreshCw, Image as ImageIcon, Edit } from "lucide-react"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -9,99 +9,170 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||||
|
import { api } from "@/lib/api"
|
||||||
|
import { showToast } from "@/lib/toast"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
interface Material {
|
interface ApiResponse<T = any> {
|
||||||
id: string
|
code: number
|
||||||
content: string
|
msg: string
|
||||||
tags: string[]
|
data: T
|
||||||
aiAnalysis?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MaterialsPage({ params }: { params: { id: string } }) {
|
interface MaterialListResponse {
|
||||||
|
list: Material[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Material {
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
coverImage: string | null
|
||||||
|
resUrls: string[]
|
||||||
|
urls: string[]
|
||||||
|
createTime: string
|
||||||
|
createMomentTime: number
|
||||||
|
time: string
|
||||||
|
wechatId: string
|
||||||
|
friendId: string | null
|
||||||
|
wechatChatroomId: number
|
||||||
|
senderNickname: string
|
||||||
|
location: string | null
|
||||||
|
lat: string
|
||||||
|
lng: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImageUrl = (url: string) => {
|
||||||
|
return /\.(jpg|jpeg|png|gif|webp)$/i.test(url) || url.includes('oss-cn-shenzhen.aliyuncs.com')
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentDisplay = ({ content, resUrls }: { content: string, resUrls: string[] }) => {
|
||||||
|
if (isImageUrl(content)) {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-48 mb-2">
|
||||||
|
<Image
|
||||||
|
src={content}
|
||||||
|
alt="素材图片"
|
||||||
|
fill
|
||||||
|
className="object-contain rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resUrls.length > 0 && resUrls.some(isImageUrl)) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||||
|
{resUrls.filter(isImageUrl).map((url, index) => (
|
||||||
|
<div key={index} className="relative w-full h-32">
|
||||||
|
<Image
|
||||||
|
src={url}
|
||||||
|
alt={`素材图片 ${index + 1}`}
|
||||||
|
fill
|
||||||
|
className="object-contain rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="text-sm text-gray-600 mb-2" style={{ whiteSpace: 'pre-line' }}>{content}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MaterialsPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const resolvedParams = use(params)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [materials, setMaterials] = useState<Material[]>([])
|
const [materials, setMaterials] = useState<Material[]>([])
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [selectedMaterial, setSelectedMaterial] = useState<Material | null>(null)
|
const [selectedMaterial, setSelectedMaterial] = useState<Material | null>(null)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const limit = 20
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const fetchMaterials = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
page: page.toString(),
|
||||||
|
limit: limit.toString(),
|
||||||
|
libraryId: resolvedParams.id,
|
||||||
|
...(searchQuery ? { keyword: searchQuery } : {})
|
||||||
|
})
|
||||||
|
const response = await api.get<ApiResponse<MaterialListResponse>>(`/v1/content/library/item-list?${queryParams.toString()}`)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
setMaterials(response.data.list)
|
||||||
|
setTotal(response.data.total)
|
||||||
|
} else {
|
||||||
|
showToast(response.msg || "获取素材数据失败", "error")
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to fetch materials:", error)
|
||||||
|
showToast(error?.message || "请检查网络连接", "error")
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [page, searchQuery, resolvedParams.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMaterials = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
// 模拟从API获取素材数据
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
const mockMaterials: Material[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
content: "今天的阳光真好,适合出去走走",
|
|
||||||
tags: ["日常", "心情"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
content: "新品上市,限时优惠,快来抢购!",
|
|
||||||
tags: ["营销", "促销"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
content: "学习新技能的第一天,感觉很充实",
|
|
||||||
tags: ["学习", "成长"],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
setMaterials(mockMaterials)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch materials:", error)
|
|
||||||
toast({
|
|
||||||
title: "错误",
|
|
||||||
description: "获取素材数据失败",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchMaterials()
|
fetchMaterials()
|
||||||
}, [])
|
}, [fetchMaterials])
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
// 实现下载功能
|
showToast("正在将素材导出为Excel格式", "loading")
|
||||||
toast({
|
|
||||||
title: "下载开始",
|
|
||||||
description: "正在将素材导出为Excel格式",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewMaterial = () => {
|
const handleNewMaterial = () => {
|
||||||
// 实现新建素材功能
|
router.push(`/content/${resolvedParams.id}/materials/new`)
|
||||||
router.push(`/content/${params.id}/materials/new`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAIAnalysis = async (material: Material) => {
|
const handleAIAnalysis = async (material: Material) => {
|
||||||
try {
|
try {
|
||||||
// 模拟AI分析过程
|
// 模拟AI分析过程
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
const analysis = "这是一条" + material.tags.join("、") + "相关的内容,情感倾向积极。"
|
const analysis = "这是一条" + material.title + "相关的内容,情感倾向积极。"
|
||||||
setMaterials(materials.map((m) => (m.id === material.id ? { ...m, aiAnalysis: analysis } : m)))
|
setSelectedMaterial(material)
|
||||||
setSelectedMaterial({ ...material, aiAnalysis: analysis })
|
showToast("AI分析完成", "success")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("AI analysis failed:", error)
|
console.error("AI analysis failed:", error)
|
||||||
toast({
|
showToast("AI分析失败", "error")
|
||||||
title: "错误",
|
|
||||||
description: "AI分析失败",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredMaterials = materials.filter(
|
const handleSearch = () => {
|
||||||
(material) =>
|
setPage(1)
|
||||||
material.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
fetchMaterials()
|
||||||
material.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div className="flex justify-center items-center h-screen">加载中...</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchMaterials()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
const loadingToast = showToast("正在删除...", "loading", true)
|
||||||
|
try {
|
||||||
|
const response = await api.delete<ApiResponse>(`/v1/content/library/delete-item?id=${id}`)
|
||||||
|
if (response.code === 200) {
|
||||||
|
showToast("删除成功", "success")
|
||||||
|
fetchMaterials()
|
||||||
|
} else {
|
||||||
|
showToast(response.msg || "删除失败", "error")
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showToast(error?.message || "删除失败", "error")
|
||||||
|
} finally {
|
||||||
|
loadingToast.remove && loadingToast.remove()
|
||||||
|
setDeleteDialogOpen(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredMaterials = materials
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<header className="sticky top-0 z-10 bg-white border-b">
|
||||||
@@ -113,10 +184,12 @@ export default function MaterialsPage({ params }: { params: { id: string } }) {
|
|||||||
<h1 className="text-lg font-medium">已采集素材</h1>
|
<h1 className="text-lg font-medium">已采集素材</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
{/* 已隐藏下载Excel按钮
|
||||||
<Button variant="outline" onClick={handleDownload}>
|
<Button variant="outline" onClick={handleDownload}>
|
||||||
<Download className="h-4 w-4 mr-2" />
|
<Download className="h-4 w-4 mr-2" />
|
||||||
下载Excel
|
下载Excel
|
||||||
</Button>
|
</Button>
|
||||||
|
*/}
|
||||||
<Button onClick={handleNewMaterial}>
|
<Button onClick={handleNewMaterial}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
新建素材
|
新建素材
|
||||||
@@ -128,53 +201,115 @@ export default function MaterialsPage({ params }: { params: { id: string } }) {
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="relative">
|
<div className="flex items-center space-x-2">
|
||||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
<div className="relative flex-1">
|
||||||
<Input
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
placeholder="搜索素材或标签..."
|
<Input
|
||||||
value={searchQuery}
|
placeholder="搜索素材..."
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
value={searchQuery}
|
||||||
className="pl-9"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
{filteredMaterials.map((material) => (
|
{isLoading ? (
|
||||||
<div key={material.id} className="flex items-center justify-between bg-white p-3 rounded-lg shadow">
|
<div className="flex justify-center items-center py-12">
|
||||||
<div className="flex-1">
|
<RefreshCw className="h-8 w-8 text-blue-500 animate-spin" />
|
||||||
<p className="text-sm text-gray-600 mb-2">{material.content}</p>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
) : filteredMaterials.length === 0 ? (
|
||||||
{material.tags.map((tag, index) => (
|
<div className="flex justify-center items-center py-12">
|
||||||
<Badge key={index} variant="secondary">
|
<div className="text-center">
|
||||||
<Tag className="h-3 w-3 mr-1" />
|
<p className="text-gray-500 mb-4">暂无数据</p>
|
||||||
{tag}
|
<Button onClick={handleNewMaterial} size="sm">
|
||||||
</Badge>
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
))}
|
新建素材
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => handleAIAnalysis(material)}>
|
|
||||||
<BarChart className="h-4 w-4 mr-1" />
|
|
||||||
AI分析
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>AI 分析结果</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="mt-4">
|
|
||||||
<p>{selectedMaterial?.aiAnalysis || "正在分析中..."}</p>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<Button variant="destructive" size="sm">
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
filteredMaterials.map((material) => (
|
||||||
|
<div
|
||||||
|
key={material.id}
|
||||||
|
className="bg-white rounded-2xl shadow-md p-5 flex flex-col gap-3 mb-4 border border-gray-100"
|
||||||
|
>
|
||||||
|
{/* 图片/内容区 */}
|
||||||
|
<ContentDisplay content={material.content} resUrls={material.resUrls} />
|
||||||
|
{/* 资源标签(非图片) */}
|
||||||
|
{material.resUrls.length > 0 && !material.resUrls.some(isImageUrl) && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-1">
|
||||||
|
{material.resUrls.map((url, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
<Tag className="h-3 w-3 mr-1" />
|
||||||
|
资源 {index + 1}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 底部信息区 */}
|
||||||
|
<div className="pt-2 border-t border-gray-100 mt-2 text-xs text-gray-500">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span>发送者: {material.senderNickname}</span>
|
||||||
|
<span>时间: {material.time}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center mt-2">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button variant="outline" size="sm" className="px-3 h-8 text-xs"
|
||||||
|
onClick={() => router.push(`/content/${resolvedParams.id}/materials/edit/${material.id}`)}>
|
||||||
|
<Edit className="h-4 w-4 mr-1" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="px-3 h-8 text-xs">
|
||||||
|
<BarChart className="h-4 w-4 mr-1" />
|
||||||
|
AI分析
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>AI 分析结果</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="mt-4">
|
||||||
|
<p>正在分析中...</p>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Dialog open={deleteDialogOpen === material.id} onOpenChange={(open) => setDeleteDialogOpen(open ? material.id : null)}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="destructive" size="sm" className="px-3 h-8 text-xs">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="mt-4 mb-4 text-sm text-gray-700">确定要删除该素材吗?此操作不可恢复。</div>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setDeleteDialogOpen(null)}>取消</Button>
|
||||||
|
<Button variant="destructive" size="sm" onClick={() => handleDelete(material.id)}>确认删除</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default function ContentLibraryPage() {
|
|||||||
],
|
],
|
||||||
creator: item.creatorName || "系统",
|
creator: item.creatorName || "系统",
|
||||||
creatorName: item.creatorName,
|
creatorName: item.creatorName,
|
||||||
itemCount: 0,
|
itemCount: item.itemCount,
|
||||||
lastUpdated: item.updateTime,
|
lastUpdated: item.updateTime,
|
||||||
enabled: item.isEnabled === 1,
|
enabled: item.isEnabled === 1,
|
||||||
// 新增字段
|
// 新增字段
|
||||||
|
|||||||
2
Server/.gitignore
vendored
2
Server/.gitignore
vendored
@@ -7,3 +7,5 @@ public/upload
|
|||||||
vendor
|
vendor
|
||||||
/public/.user.ini
|
/public/.user.ini
|
||||||
/404.html
|
/404.html
|
||||||
|
# SpecStory explanation file
|
||||||
|
.specstory/.what-is-this.md
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ Route::group('v1/', function () {
|
|||||||
Route::delete('delete', 'app\cunkebao\controller\ContentLibraryController@delete'); // 删除内容库
|
Route::delete('delete', 'app\cunkebao\controller\ContentLibraryController@delete'); // 删除内容库
|
||||||
Route::get('detail', 'app\cunkebao\controller\ContentLibraryController@detail'); // 获取内容库详情
|
Route::get('detail', 'app\cunkebao\controller\ContentLibraryController@detail'); // 获取内容库详情
|
||||||
Route::get('collectMoments', 'app\cunkebao\controller\ContentLibraryController@collectMoments'); // 采集朋友圈
|
Route::get('collectMoments', 'app\cunkebao\controller\ContentLibraryController@collectMoments'); // 采集朋友圈
|
||||||
|
Route::get('item-list', 'app\cunkebao\controller\ContentLibraryController@getItemList'); // 获取内容库素材列表
|
||||||
|
Route::post('add-item', 'app\cunkebao\controller\ContentLibraryController@addItem'); // 添加内容库素材
|
||||||
|
Route::delete('delete-item', 'app\cunkebao\controller\ContentLibraryController@deleteItem'); // 删除内容库素材
|
||||||
|
Route::get('get-item-detail', 'app\cunkebao\controller\ContentLibraryController@getItemDetail'); // 获取内容库素材详情
|
||||||
});
|
});
|
||||||
|
|
||||||
// 好友相关
|
// 好友相关
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use app\api\controller\WebSocketController;
|
|||||||
class ContentLibraryController extends Controller
|
class ContentLibraryController extends Controller
|
||||||
{
|
{
|
||||||
/************************************
|
/************************************
|
||||||
* 内容库管理相关功能
|
* 内容库基础管理功能
|
||||||
************************************/
|
************************************/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +138,7 @@ class ContentLibraryController extends Controller
|
|||||||
$item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true);
|
$item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true);
|
||||||
// 添加创建人名称
|
// 添加创建人名称
|
||||||
$item['creatorName'] = $item['user']['username'] ?? '';
|
$item['creatorName'] = $item['user']['username'] ?? '';
|
||||||
|
$item['itemCount'] = Db::name('content_item')->where('libraryId', $item['id'])->count();
|
||||||
|
|
||||||
// 获取好友详细信息
|
// 获取好友详细信息
|
||||||
if (!empty($item['sourceFriends'] && $item['sourceType'] == 1)) {
|
if (!empty($item['sourceFriends'] && $item['sourceType'] == 1)) {
|
||||||
@@ -373,9 +373,97 @@ class ContentLibraryController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/************************************
|
/************************************
|
||||||
* 内容项目管理相关功能
|
* 内容项目管理功能
|
||||||
************************************/
|
************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内容库素材列表
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getItemList()
|
||||||
|
{
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$libraryId = $this->request->param('libraryId', 0);
|
||||||
|
$keyword = $this->request->param('keyword', ''); // 搜索关键词
|
||||||
|
|
||||||
|
if (empty($libraryId)) {
|
||||||
|
return json(['code' => 400, 'msg' => '内容库ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证内容库权限
|
||||||
|
$library = ContentLibrary::where([
|
||||||
|
['id', '=', $libraryId],
|
||||||
|
['userId', '=', $this->request->userInfo['id']],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (empty($library)) {
|
||||||
|
return json(['code' => 404, 'msg' => '内容库不存在或无权限访问']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
$where = [
|
||||||
|
['libraryId', '=', $libraryId],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (!empty($keyword)) {
|
||||||
|
$where[] = ['content', 'like', '%' . $keyword . '%'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
$list = ContentItem::where($where)
|
||||||
|
->field('id,type,title,content,coverImage,resUrls,urls,createTime,createMomentTime,createMessageTime,wechatId,friendId,wechatChatroomId,senderNickname,location,lat,lng')
|
||||||
|
->order('createTime', 'desc')
|
||||||
|
->page($page, $limit)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
// 处理数据
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
// 处理资源URL
|
||||||
|
$item['resUrls'] = json_decode($item['resUrls'] ?: '[]', true);
|
||||||
|
$item['urls'] = json_decode($item['urls'] ?: '[]', true);
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
//$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
|
||||||
|
if ($item['createMomentTime']) {
|
||||||
|
$item['time'] = date('Y-m-d H:i:s', $item['createMomentTime']);
|
||||||
|
}
|
||||||
|
if ($item['createMessageTime']) {
|
||||||
|
$item['time'] = date('Y-m-d H:i:s', $item['createMessageTime']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取发送者信息
|
||||||
|
if ($item['type'] == 'moment' && $item['friendId']) {
|
||||||
|
$friendInfo = Db::name('wechat_friend')
|
||||||
|
->alias('wf')
|
||||||
|
->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
|
||||||
|
->where('wf.id', $item['friendId'])
|
||||||
|
->field('wa.nickname, wa.avatar')
|
||||||
|
->find();
|
||||||
|
$item['senderNickname'] = $friendInfo['nickname'] ?: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
$total = ContentItem::where($where)->count();
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => [
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加内容项目
|
* 添加内容项目
|
||||||
* @return \think\response\Json
|
* @return \think\response\Json
|
||||||
@@ -401,6 +489,18 @@ class ContentLibraryController extends Controller
|
|||||||
if (empty($param['contentData'])) {
|
if (empty($param['contentData'])) {
|
||||||
return json(['code' => 400, 'msg' => '内容数据不能为空']);
|
return json(['code' => 400, 'msg' => '内容数据不能为空']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当类型为群消息时,限制图片只能上传一张
|
||||||
|
if ($param['type'] == 'group_message') {
|
||||||
|
$images = isset($param['images']) ? $param['images'] : [];
|
||||||
|
if (is_string($images)) {
|
||||||
|
$images = json_decode($images, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($images) > 1) {
|
||||||
|
return json(['code' => 400, 'msg' => '群消息类型只能上传一张图片']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 查询内容库是否存在
|
// 查询内容库是否存在
|
||||||
$library = ContentLibrary::where([
|
$library = ContentLibrary::where([
|
||||||
@@ -432,8 +532,10 @@ class ContentLibraryController extends Controller
|
|||||||
* @param int $id 内容项目ID
|
* @param int $id 内容项目ID
|
||||||
* @return \think\response\Json
|
* @return \think\response\Json
|
||||||
*/
|
*/
|
||||||
public function deleteItem($id)
|
public function deleteItem()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
if (empty($id)) {
|
if (empty($id)) {
|
||||||
return json(['code' => 400, 'msg' => '参数错误']);
|
return json(['code' => 400, 'msg' => '参数错误']);
|
||||||
}
|
}
|
||||||
@@ -453,7 +555,11 @@ class ContentLibraryController extends Controller
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 删除内容项目
|
// 删除内容项目
|
||||||
ContentItem::destroy($id);
|
$service = new \app\cunkebao\service\ContentItemService();
|
||||||
|
$result = $service->deleteItem($id);
|
||||||
|
if ($result['code'] != 200) {
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
|
||||||
return json(['code' => 200, 'msg' => '删除成功']);
|
return json(['code' => 200, 'msg' => '删除成功']);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@@ -461,6 +567,83 @@ class ContentLibraryController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内容项目详情
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getItemDetail()
|
||||||
|
{
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
if (empty($id)) {
|
||||||
|
return json(['code' => 400, 'msg' => '参数错误']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询内容项目是否存在并检查权限
|
||||||
|
$item = ContentItem::alias('i')
|
||||||
|
->join('content_library l', 'i.libraryId = l.id')
|
||||||
|
->where([
|
||||||
|
['i.id', '=', $id],
|
||||||
|
['l.userId', '=', $this->request->userInfo['id']],
|
||||||
|
['i.isDel', '=', 0]
|
||||||
|
])
|
||||||
|
->field('i.*')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (empty($item)) {
|
||||||
|
return json(['code' => 404, 'msg' => '内容项目不存在或无权限访问']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数据
|
||||||
|
// 处理资源URL
|
||||||
|
$item['resUrls'] = json_decode($item['resUrls'] ?: '[]', true);
|
||||||
|
$item['urls'] = json_decode($item['urls'] ?: '[]', true);
|
||||||
|
|
||||||
|
// 添加内容类型的文字描述
|
||||||
|
$contentTypeMap = [
|
||||||
|
0 => '未知',
|
||||||
|
1 => '图片',
|
||||||
|
2 => '链接',
|
||||||
|
3 => '视频',
|
||||||
|
4 => '文本',
|
||||||
|
5 => '小程序',
|
||||||
|
6 => '图文'
|
||||||
|
];
|
||||||
|
$item['contentTypeName'] = $contentTypeMap[$item['contentType'] ?? 0] ?? '未知';
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
if ($item['createMomentTime']) {
|
||||||
|
$item['createMomentTimeFormatted'] = date('Y-m-d H:i:s', $item['createMomentTime']);
|
||||||
|
}
|
||||||
|
if ($item['createMessageTime']) {
|
||||||
|
$item['createMessageTimeFormatted'] = date('Y-m-d H:i:s', $item['createMessageTime']);
|
||||||
|
}
|
||||||
|
//$item['createTimeFormatted'] = date('Y-m-d H:i:s', $item['createTime']);
|
||||||
|
|
||||||
|
// 获取发送者信息
|
||||||
|
if ($item['type'] == 'moment' && $item['friendId']) {
|
||||||
|
$friendInfo = Db::name('wechat_friend')
|
||||||
|
->alias('wf')
|
||||||
|
->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
|
||||||
|
->where('wf.id', $item['friendId'])
|
||||||
|
->field('wa.nickname, wa.avatar')
|
||||||
|
->find();
|
||||||
|
$item['senderInfo'] = $friendInfo ?: [];
|
||||||
|
} elseif ($item['type'] == 'group_message' && $item['wechatChatroomId']) {
|
||||||
|
// 获取群组信息
|
||||||
|
$groupInfo = Db::name('wechat_group')
|
||||||
|
->where('id', $item['wechatChatroomId'])
|
||||||
|
->field('name, avatar')
|
||||||
|
->find();
|
||||||
|
$item['groupInfo'] = $groupInfo ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => $item
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/************************************
|
/************************************
|
||||||
* 数据采集相关功能
|
* 数据采集相关功能
|
||||||
************************************/
|
************************************/
|
||||||
@@ -708,81 +891,6 @@ class ContentLibraryController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存朋友圈数据到内容项目表
|
|
||||||
* @param array $moment 朋友圈数据
|
|
||||||
* @param int $libraryId 内容库ID
|
|
||||||
* @param array $friend 好友信息
|
|
||||||
* @param string $nickname 好友昵称
|
|
||||||
* @return bool 是否保存成功
|
|
||||||
*/
|
|
||||||
private function saveMomentToContentItem($moment, $libraryId, $friend, $nickname)
|
|
||||||
{
|
|
||||||
if (empty($moment) || empty($libraryId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// 检查朋友圈数据是否已存在于内容项目中
|
|
||||||
$exists = ContentItem::where('libraryId', $libraryId)
|
|
||||||
->where('snsId', $moment['snsId'] ?? '')
|
|
||||||
->find();
|
|
||||||
|
|
||||||
if ($exists) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析资源URL (可能是JSON字符串)
|
|
||||||
$resUrls = $moment['resUrls'];
|
|
||||||
if (is_string($resUrls)) {
|
|
||||||
$resUrls = json_decode($resUrls, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理urls字段
|
|
||||||
$urls = $moment['urls'] ?? [];
|
|
||||||
if (is_string($urls)) {
|
|
||||||
$urls = json_decode($urls, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建封面图片
|
|
||||||
$coverImage = '';
|
|
||||||
if (!empty($resUrls) && is_array($resUrls) && count($resUrls) > 0) {
|
|
||||||
$coverImage = $resUrls[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不存在,则创建新的内容项目
|
|
||||||
$item = new ContentItem();
|
|
||||||
$item->libraryId = $libraryId;
|
|
||||||
$item->type = 'moment'; // 朋友圈类型
|
|
||||||
$item->title = '来自 ' . $nickname . ' 的朋友圈';
|
|
||||||
$item->contentData = json_encode($moment, JSON_UNESCAPED_UNICODE);
|
|
||||||
$item->snsId = $moment['snsId'] ?? ''; // 存储snsId便于后续查询
|
|
||||||
$item->createTime = time();
|
|
||||||
$item->wechatId = $friend['wechatId'];
|
|
||||||
$item->friendId = $friend['id'];
|
|
||||||
$item->createMomentTime = $moment['createTime'] ?? 0;
|
|
||||||
$item->content = $moment['content'] ?? '';
|
|
||||||
$item->coverImage = $coverImage;
|
|
||||||
|
|
||||||
// 独立存储resUrls和urls字段
|
|
||||||
$item->resUrls = is_string($moment['resUrls']) ? $moment['resUrls'] : json_encode($resUrls, JSON_UNESCAPED_UNICODE);
|
|
||||||
$item->urls = is_string($moment['urls']) ? $moment['urls'] : json_encode($urls, JSON_UNESCAPED_UNICODE);
|
|
||||||
|
|
||||||
// 保存地理位置信息
|
|
||||||
$item->location = $moment['location'] ?? '';
|
|
||||||
$item->lat = $moment['lat'] ?? 0;
|
|
||||||
$item->lng = $moment['lng'] ?? 0;
|
|
||||||
$item->save();
|
|
||||||
return true;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// 记录错误日志
|
|
||||||
\think\facade\Log::error('保存朋友圈数据失败: ' . $e->getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从群组采集消息内容
|
* 从群组采集消息内容
|
||||||
* @param array $library 内容库配置
|
* @param array $library 内容库配置
|
||||||
@@ -973,6 +1081,176 @@ class ContentLibraryController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断内容类型
|
||||||
|
* @param string $content 内容文本
|
||||||
|
* @param array $resUrls 资源URL数组
|
||||||
|
* @param array $urls URL数组
|
||||||
|
* @return int 内容类型: 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
|
||||||
|
*/
|
||||||
|
private function determineContentType($content, $resUrls = [], $urls = [])
|
||||||
|
{
|
||||||
|
// 判断是否为空
|
||||||
|
if (empty($content) && empty($resUrls) && empty($urls)) {
|
||||||
|
return 0; // 未知类型
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否有小程序信息
|
||||||
|
if (strpos($content, '小程序') !== false || strpos($content, 'appid') !== false) {
|
||||||
|
return 5; // 小程序
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查资源URL中是否有视频或图片
|
||||||
|
$hasVideo = false;
|
||||||
|
$hasImage = false;
|
||||||
|
|
||||||
|
if (!empty($resUrls)) {
|
||||||
|
foreach ($resUrls as $url) {
|
||||||
|
// 检查是否为视频文件
|
||||||
|
if (stripos($url, '.mp4') !== false ||
|
||||||
|
stripos($url, '.mov') !== false ||
|
||||||
|
stripos($url, '.avi') !== false ||
|
||||||
|
stripos($url, '.wmv') !== false ||
|
||||||
|
stripos($url, '.flv') !== false ||
|
||||||
|
stripos($url, 'video') !== false) {
|
||||||
|
$hasVideo = true;
|
||||||
|
break; // 一旦发现视频文件,立即退出循环
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为图片文件
|
||||||
|
if (stripos($url, '.jpg') !== false ||
|
||||||
|
stripos($url, '.jpeg') !== false ||
|
||||||
|
stripos($url, '.png') !== false ||
|
||||||
|
stripos($url, '.gif') !== false ||
|
||||||
|
stripos($url, '.webp') !== false ||
|
||||||
|
stripos($url, '.bmp') !== false ||
|
||||||
|
stripos($url, 'image') !== false) {
|
||||||
|
$hasImage = true;
|
||||||
|
// 不退出循环,继续检查是否有视频(视频优先级更高)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果发现视频文件,判定为视频类型
|
||||||
|
if ($hasVideo) {
|
||||||
|
return 3; // 视频
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先判断内容文本
|
||||||
|
// 如果有文本内容(不仅仅是链接)
|
||||||
|
if (!empty($content)) {
|
||||||
|
// 判断内容是否主要为文本(排除链接部分)
|
||||||
|
$contentWithoutUrls = $content;
|
||||||
|
if (!empty($urls)) {
|
||||||
|
foreach ($urls as $url) {
|
||||||
|
$contentWithoutUrls = str_replace($url, '', $contentWithoutUrls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果去除链接后仍有文本内容(不考虑长度)
|
||||||
|
if (!empty(trim($contentWithoutUrls))) {
|
||||||
|
// 判断是否为图文类型
|
||||||
|
if ($hasImage) {
|
||||||
|
return 6; // 图文
|
||||||
|
} else {
|
||||||
|
return 4; // 纯文本
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为图片类型
|
||||||
|
if ($hasImage) {
|
||||||
|
return 1; // 图片
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为链接类型
|
||||||
|
if (!empty($urls)) {
|
||||||
|
return 2; // 链接
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认为文本类型
|
||||||
|
return 4; // 文本
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存朋友圈数据到内容项目表
|
||||||
|
* @param array $moment 朋友圈数据
|
||||||
|
* @param int $libraryId 内容库ID
|
||||||
|
* @param array $friend 好友信息
|
||||||
|
* @param string $nickname 好友昵称
|
||||||
|
* @return bool 是否保存成功
|
||||||
|
*/
|
||||||
|
private function saveMomentToContentItem($moment, $libraryId, $friend, $nickname)
|
||||||
|
{
|
||||||
|
if (empty($moment) || empty($libraryId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// 检查朋友圈数据是否已存在于内容项目中
|
||||||
|
$exists = ContentItem::where('libraryId', $libraryId)
|
||||||
|
->where('snsId', $moment['snsId'] ?? '')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析资源URL (可能是JSON字符串)
|
||||||
|
$resUrls = $moment['resUrls'];
|
||||||
|
if (is_string($resUrls)) {
|
||||||
|
$resUrls = json_decode($resUrls, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理urls字段
|
||||||
|
$urls = $moment['urls'] ?? [];
|
||||||
|
if (is_string($urls)) {
|
||||||
|
$urls = json_decode($urls, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建封面图片
|
||||||
|
$coverImage = '';
|
||||||
|
if (!empty($resUrls) && is_array($resUrls) && count($resUrls) > 0) {
|
||||||
|
$coverImage = $resUrls[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文)
|
||||||
|
$contentType = $this->determineContentType($moment['content'] ?? '', $resUrls, $urls);
|
||||||
|
|
||||||
|
// 如果不存在,则创建新的内容项目
|
||||||
|
$item = new ContentItem();
|
||||||
|
$item->libraryId = $libraryId;
|
||||||
|
$item->type = 'moment'; // 朋友圈类型
|
||||||
|
$item->title = '来自 ' . $nickname . ' 的朋友圈';
|
||||||
|
$item->contentData = json_encode($moment, JSON_UNESCAPED_UNICODE);
|
||||||
|
$item->snsId = $moment['snsId'] ?? ''; // 存储snsId便于后续查询
|
||||||
|
$item->createTime = time();
|
||||||
|
$item->wechatId = $friend['wechatId'];
|
||||||
|
$item->friendId = $friend['id'];
|
||||||
|
$item->createMomentTime = $moment['createTime'] ?? 0;
|
||||||
|
$item->content = $moment['content'] ?? '';
|
||||||
|
$item->coverImage = $coverImage;
|
||||||
|
$item->contentType = $contentType; // 设置内容类型
|
||||||
|
|
||||||
|
// 独立存储resUrls和urls字段
|
||||||
|
$item->resUrls = is_string($moment['resUrls']) ? $moment['resUrls'] : json_encode($resUrls, JSON_UNESCAPED_UNICODE);
|
||||||
|
$item->urls = is_string($moment['urls']) ? $moment['urls'] : json_encode($urls, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
// 保存地理位置信息
|
||||||
|
$item->location = $moment['location'] ?? '';
|
||||||
|
$item->lat = $moment['lat'] ?? 0;
|
||||||
|
$item->lng = $moment['lng'] ?? 0;
|
||||||
|
$item->save();
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 记录错误日志
|
||||||
|
\think\facade\Log::error('保存朋友圈数据失败: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存群聊消息到内容项目表
|
* 保存群聊消息到内容项目表
|
||||||
* @param array $message 消息数据
|
* @param array $message 消息数据
|
||||||
@@ -1006,6 +1284,15 @@ class ContentLibraryController extends Controller
|
|||||||
$links = $matches[0];
|
$links = $matches[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取可能的图片URL
|
||||||
|
$resUrls = [];
|
||||||
|
if (isset($message['imageUrl']) && !empty($message['imageUrl'])) {
|
||||||
|
$resUrls[] = $message['imageUrl'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文)
|
||||||
|
$contentType = $this->determineContentType($content, $resUrls, $links);
|
||||||
|
|
||||||
// 创建新的内容项目
|
// 创建新的内容项目
|
||||||
$item = new ContentItem();
|
$item = new ContentItem();
|
||||||
$item->libraryId = $libraryId;
|
$item->libraryId = $libraryId;
|
||||||
@@ -1015,6 +1302,7 @@ class ContentLibraryController extends Controller
|
|||||||
$item->msgId = $message['msgId'] ?? ''; // 存储msgId便于后续查询
|
$item->msgId = $message['msgId'] ?? ''; // 存储msgId便于后续查询
|
||||||
$item->createTime = time();
|
$item->createTime = time();
|
||||||
$item->content = $content;
|
$item->content = $content;
|
||||||
|
$item->contentType = $contentType; // 设置内容类型
|
||||||
|
|
||||||
// 设置发送者信息
|
// 设置发送者信息
|
||||||
$item->wechatId = $message['senderWechatId'] ?? '';
|
$item->wechatId = $message['senderWechatId'] ?? '';
|
||||||
@@ -1022,9 +1310,18 @@ class ContentLibraryController extends Controller
|
|||||||
$item->senderNickname = $message['senderNickname'] ?? '';
|
$item->senderNickname = $message['senderNickname'] ?? '';
|
||||||
$item->createMessageTime = $message['createTime'] ?? 0;
|
$item->createMessageTime = $message['createTime'] ?? 0;
|
||||||
|
|
||||||
|
// 处理资源URL
|
||||||
|
if (!empty($resUrls)) {
|
||||||
|
$item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE);
|
||||||
|
// 设置封面图片
|
||||||
|
if (!empty($resUrls[0])) {
|
||||||
|
$item->coverImage = $resUrls[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理链接
|
// 处理链接
|
||||||
if (!empty($links)) {
|
if (!empty($links)) {
|
||||||
$item->resUrls = json_encode($links, JSON_UNESCAPED_UNICODE);
|
$item->urls = json_encode($links, JSON_UNESCAPED_UNICODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置商品信息(需根据消息内容解析)
|
// 设置商品信息(需根据消息内容解析)
|
||||||
@@ -1077,7 +1374,7 @@ class ContentLibraryController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取朋友圈数据(示例方法,实际实现需要根据具体API)
|
* 获取朋友圈数据
|
||||||
* @param string $wechatId 微信ID
|
* @param string $wechatId 微信ID
|
||||||
* @return array 朋友圈数据
|
* @return array 朋友圈数据
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ContentItemService
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = ContentItem::where('id', $itemId)
|
$result = ContentItem::where('id', $itemId)
|
||||||
->update(['isDel' => 1, 'updateTime' => time()]);
|
->update(['isDel' => 1, 'delTime' => time()]);
|
||||||
|
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
return ['code' => 500, 'msg' => '删除失败'];
|
return ['code' => 500, 'msg' => '删除失败'];
|
||||||
|
|||||||
Reference in New Issue
Block a user