【操盘手】 内容库优化

This commit is contained in:
wong
2025-05-14 17:25:40 +08:00
parent 2ad1b99ea3
commit 8b98ff3992
4 changed files with 383 additions and 144 deletions

View File

@@ -4,7 +4,7 @@ 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 { ChevronLeft, Plus, X, Image as ImageIcon, UploadCloud, Link, Video, FileText, Layers } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
@@ -13,6 +13,8 @@ import { toast } from "@/components/ui/use-toast"
import { api } from "@/lib/api"
import { showToast } from "@/lib/toast"
import Image from "next/image"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
interface ApiResponse<T = any> {
code: number
@@ -44,6 +46,15 @@ const isImageUrl = (url: string) => {
return /\.(jpg|jpeg|png|gif|webp)$/i.test(url) || url.includes('oss-cn-shenzhen.aliyuncs.com')
}
// 素材类型枚举
const MATERIAL_TYPES = [
{ id: 1, name: "图片", icon: ImageIcon },
{ id: 2, name: "链接", icon: Link },
{ id: 3, name: "视频", icon: Video },
{ id: 4, name: "文本", icon: FileText },
{ id: 5, name: "小程序", icon: Layers }
]
export default function EditMaterialPage({ params }: { params: Promise<{ id: string, materialId: string }> }) {
const resolvedParams = use(params)
const router = useRouter()
@@ -52,6 +63,8 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str
const [images, setImages] = useState<string[]>([])
const [previewUrls, setPreviewUrls] = useState<string[]>([])
const [originalMaterial, setOriginalMaterial] = useState<Material | null>(null)
const [materialType, setMaterialType] = useState<number>(1) // 默认为图片类型
const [url, setUrl] = useState<string>("")
// 获取素材详情
useEffect(() => {
@@ -65,6 +78,14 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str
setOriginalMaterial(material)
setContent(material.content)
// 设置素材类型
setMaterialType(Number(material.type) || 1)
// 如果是链接类型设置URL
if (material.type === "2" && material.urls && material.urls.length > 0) {
setUrl(material.urls[0])
}
// 处理图片
const imageUrls: string[] = []
@@ -126,18 +147,37 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!content && images.length === 0) {
showToast("请输入素材内容或上传图片", "error")
// 根据不同类型校验不同字段
if (materialType === 1 && images.length === 0) {
showToast("请上传图片", "error")
return
} else if (materialType === 2 && !url) {
showToast("请输入链接地址", "error")
return
} else if ((materialType === 4 || materialType === 6) && !content) {
showToast("请输入文本内容", "error")
return
}
const loadingToast = showToast("正在更新素材...", "loading", true)
try {
const response = await api.post<ApiResponse>('/v1/content/library/update-item', {
const payload: any = {
id: resolvedParams.materialId,
type: materialType,
content: content,
resUrls: images
})
}
// 根据类型添加不同的字段
if (materialType === 1) {
payload.resUrls = images
} else if (materialType === 2) {
payload.urls = [url]
} else if (materialType === 3) {
payload.urls = [url]
}
const response = await api.post<ApiResponse>('/v1/content/library/update-item', payload)
if (response.code === 200) {
showToast("素材更新成功", "success")
@@ -177,8 +217,31 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str
<div className="p-4">
<Card className="p-4">
<form onSubmit={handleSubmit} className="space-y-4">
{/* 只有当内容不是图片链接时才显示内容编辑区 */}
{!isImageUrl(content) && (
{/* 素材类型选择器 */}
<div>
<Label className="text-base required"></Label>
<div className="flex items-center mt-2 border border-gray-200 rounded-md overflow-hidden">
{MATERIAL_TYPES.map((type) => (
<button
key={type.id}
type="button"
className={cn(
"flex-1 py-2 px-4 flex items-center justify-center gap-1 text-sm transition-colors",
materialType === type.id
? "bg-blue-500 text-white"
: "bg-white text-gray-600 hover:bg-gray-50"
)}
onClick={() => setMaterialType(type.id)}
>
<type.icon className="h-4 w-4" />
{type.name}
</button>
))}
</div>
</div>
{/* 根据不同类型显示不同的编辑区域 */}
{(materialType === 4 || materialType === 6 || (materialType === 1 && !isImageUrl(content))) && (
<div>
<Label htmlFor="content"></Label>
<Textarea
@@ -192,50 +255,103 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str
</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"> JPGPNG </span>
</Button>
{/* 链接或视频类型 */}
{(materialType === 2 || materialType === 3) && (
<div>
<Label htmlFor="url">{materialType === 2 ? "链接地址" : "视频链接"}</Label>
<Input
id="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder={materialType === 2 ? "请输入链接地址" : "请输入视频链接"}
className="mt-1"
/>
</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"
/>
{/* 图片类型 */}
{materialType === 1 && (
<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"> JPGPNG </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>
<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>
)}
{/* 小程序类型 */}
{materialType === 5 && (
<div className="space-y-4">
<div>
<Label htmlFor="appTitle"></Label>
<Input
id="appTitle"
placeholder="请输入小程序名称"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="appId">AppID</Label>
<Input
id="appId"
placeholder="请输入AppID"
className="mt-1"
/>
</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-4 flex flex-col items-center justify-center"
>
<UploadCloud className="h-6 w-6 mb-2 text-gray-400" />
<span></span>
</Button>
</div>
</div>
)}
</div>
</div>
)}
<Button type="submit" className="w-full">

View File

@@ -4,19 +4,41 @@ import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Plus, X, Image as ImageIcon, UploadCloud } from "lucide-react"
import { ChevronLeft, Plus, X, Image as ImageIcon, UploadCloud, Link, Video, FileText, Layers } 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 Image from "next/image"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import { api } from "@/lib/api"
import { showToast } from "@/lib/toast"
interface ApiResponse<T = any> {
code: number
msg: string
data: T
}
// 素材类型枚举
const MATERIAL_TYPES = [
{ id: 1, name: "图片", icon: ImageIcon },
{ id: 2, name: "链接", icon: Link },
{ id: 3, name: "视频", icon: Video },
{ id: 4, name: "文本", icon: FileText },
{ id: 5, name: "小程序", icon: Layers }
]
export default function NewMaterialPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [content, setContent] = useState("")
const [images, setImages] = useState<string[]>([])
const [previewUrls, setPreviewUrls] = useState<string[]>([])
const [materialType, setMaterialType] = useState<number>(1) // 默认为图片类型
const [url, setUrl] = useState<string>("")
const [loading, setLoading] = useState(false)
// 模拟上传图片
const handleUploadImage = () => {
@@ -44,29 +66,53 @@ export default function NewMaterialPage({ params }: { params: { id: string } })
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!content && images.length === 0) {
toast({
title: "错误",
description: "请输入素材内容或上传图片",
variant: "destructive",
})
// 根据不同类型校验不同字段
if (materialType === 1 && images.length === 0) {
showToast("请上传图片", "error")
return
} else if (materialType === 2 && !url) {
showToast("请输入链接地址", "error")
return
} else if ((materialType === 4 || materialType === 6) && !content) {
showToast("请输入文本内容", "error")
return
}
setLoading(true)
const loadingToast = showToast("正在创建素材...", "loading", true)
try {
// 模拟保存新素材
await new Promise((resolve) => setTimeout(resolve, 1000))
toast({
title: "成功",
description: "新素材已创建",
})
router.push(`/content/${params.id}/materials`)
} catch (error) {
// 构建API请求参数
const payload: any = {
libraryId: params.id,
type: materialType,
content: content,
}
// 根据类型添加不同的字段
if (materialType === 1) {
payload.resUrls = images
} else if (materialType === 2) {
payload.urls = [url]
} else if (materialType === 3) {
payload.urls = [url]
}
const response = await api.post<ApiResponse>('/v1/content/library/create-item', payload)
if (response.code === 200) {
showToast("创建成功", "success")
router.push(`/content/${params.id}/materials`)
} else {
showToast(response.msg || "创建失败", "error")
}
} catch (error: any) {
console.error("Failed to create new material:", error)
toast({
title: "错误",
description: "创建新素材失败",
variant: "destructive",
})
showToast(error?.message || "创建素材失败", "error")
} finally {
loadingToast.remove && loadingToast.remove()
setLoading(false)
}
}
@@ -86,65 +132,144 @@ export default function NewMaterialPage({ params }: { params: { id: string } })
<div className="p-4">
<Card className="p-4">
<form onSubmit={handleSubmit} className="space-y-4">
{/* 素材类型选择器 */}
<div>
<Label htmlFor="content"></Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="请输入素材内容"
className="mt-1"
rows={10}
/>
<Label className="text-base required"></Label>
<div className="flex items-center mt-2 border border-gray-200 rounded-md overflow-hidden">
{MATERIAL_TYPES.map((type) => (
<button
key={type.id}
type="button"
className={cn(
"flex-1 py-2 px-4 flex items-center justify-center gap-1 text-sm transition-colors",
materialType === type.id
? "bg-blue-500 text-white"
: "bg-white text-gray-600 hover:bg-gray-50"
)}
onClick={() => setMaterialType(type.id)}
>
<type.icon className="h-4 w-4" />
{type.name}
</button>
))}
</div>
</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"> JPGPNG </span>
</Button>
{/* 根据不同类型显示不同的编辑区域 */}
{(materialType === 4 || materialType === 6) && (
<div>
<Label htmlFor="content"></Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="请输入素材内容"
className="mt-1"
rows={10}
/>
</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"
/>
{/* 链接或视频类型 */}
{(materialType === 2 || materialType === 3) && (
<div>
<Label htmlFor="url">{materialType === 2 ? "链接地址" : "视频链接"}</Label>
<Input
id="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder={materialType === 2 ? "请输入链接地址" : "请输入视频链接"}
className="mt-1"
/>
</div>
)}
{/* 图片类型 */}
{materialType === 1 && (
<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"> JPGPNG </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>
<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>
)}
{/* 小程序类型 */}
{materialType === 5 && (
<div className="space-y-4">
<div>
<Label htmlFor="appTitle"></Label>
<Input
id="appTitle"
placeholder="请输入小程序名称"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="appId">AppID</Label>
<Input
id="appId"
placeholder="请输入AppID"
className="mt-1"
/>
</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-4 flex flex-col items-center justify-center"
>
<UploadCloud className="h-6 w-6 mb-2 text-gray-400" />
<span></span>
</Button>
</div>
</div>
)}
</div>
</div>
)}
<Button type="submit" className="w-full">
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "创建中..." : "保存素材"}
</Button>
</form>
</Card>

View File

@@ -258,6 +258,10 @@ export default function MaterialsPage({ params }: { params: Promise<{ id: string
}
}
const handleSubmit = async () => {
fetchMaterials()
}
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<header className="sticky top-0 z-10 bg-white border-b">
@@ -314,7 +318,7 @@ export default function MaterialsPage({ params }: { params: Promise<{ id: string
</div>
</div>
<div className="my-3 h-0.5 bg-gray-100"></div>
<div className="space-y-2">
<div className="space-y-2">
<div className="h-4 w-full bg-gray-200 animate-pulse rounded"></div>
<div className="h-4 w-3/4 bg-gray-200 animate-pulse rounded"></div>
<div className="flex space-x-2 mt-3">
@@ -389,18 +393,18 @@ export default function MaterialsPage({ params }: { params: Promise<{ id: string
<Edit className="h-4 w-4 mr-1" />
</Button>
<Dialog>
<DialogTrigger asChild>
<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">
<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>
@@ -427,7 +431,7 @@ export default function MaterialsPage({ params }: { params: Promise<{ id: string
</Card>
))
)}
</div>
</div>
)}
{!isLoading && total > limit && (

View File

@@ -449,16 +449,12 @@ class ContentLibraryController extends Controller
$item['senderNickname'] = $friendInfo['nickname'] ?: '';
$item['senderAvatar'] = $friendInfo['avatar'] ?: '';
}else{
// $friendInfo = Db::name('wechat_group_member')
// ->alias('wgm')
// ->join('wechat_account wa', 'wgm.identifier = wa.wechatId')
// ->where('wgm.identifier', $item['wechatId'])
// ->field('wa.nickname, wa.avatar')
// ->find();
// print_r($friendInfo);
// exit;
// $item['senderNickname'] = $friendInfo['nickname'] ?: '';
// $item['senderAvatar'] = $friendInfo['avatar'] ?: '';
$friendInfo = Db::table('s2_wechat_chatroom_member')
->field('nickname, avatar')
->where('wechatId', $item['wechatId'])
->find();
$item['senderNickname'] = $friendInfo['nickname'] ?: '';
$item['senderAvatar'] = $friendInfo['avatar'] ?: '';
}
}
unset($item);
@@ -614,13 +610,11 @@ class ContentLibraryController extends Controller
// 添加内容类型的文字描述
$contentTypeMap = [
0 => '未知',
1 => '图片',
2 => '链接',
3 => '视频',
4 => '文本',
5 => '小程序',
6 => '图文'
];
$item['contentTypeName'] = $contentTypeMap[$item['contentType'] ?? 0] ?? '未知';
@@ -1120,7 +1114,7 @@ class ContentLibraryController extends Controller
* @param string $content 内容文本
* @param array $resUrls 资源URL数组
* @param array $urls URL数组
* @return int 内容类型: 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
* @return int 内容类型: 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序
*/
private function determineContentType($content, $resUrls = [], $urls = [])
{
@@ -1237,7 +1231,7 @@ class ContentLibraryController extends Controller
if (!empty($content) && !$isPureLink) {
// 如果有图片,则为图文类型
if ($hasImage) {
return 6; // 图文
return 1; // 图文
} else {
return 4; // 纯文本
}