refactor: overhaul UI for streamlined user experience

Redesign navigation, home overview, user portrait, and valuation pages
with improved functionality and responsive design.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-18 13:47:12 +00:00
parent 440b310c6f
commit 2408d50cb0
316 changed files with 55785 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
export default function Loading() {
return (
<div className="container mx-auto py-6 space-y-6">
<div className="flex justify-between items-center">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-10 w-32" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array(6)
.fill(0)
.map((_, i) => (
<Card key={i} className="overflow-hidden">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<Skeleton className="h-6 w-40" />
<Skeleton className="h-6 w-20" />
</div>
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-40 w-full" />
<div className="flex justify-between items-center">
<Skeleton className="h-5 w-24" />
<div className="flex space-x-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
</div>
</CardContent>
</Card>
))}
</div>
<div className="flex justify-center mt-6">
<Skeleton className="h-10 w-64" />
</div>
</div>
)
}

View File

@@ -0,0 +1,130 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Plus, X } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import { toast } from "@/components/ui/use-toast"
export default function NewMaterialPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [content, setContent] = useState("")
const [newTag, setNewTag] = useState("")
const [tags, setTags] = useState<string[]>([])
const handleAddTag = () => {
if (newTag && !tags.includes(newTag)) {
setTags([...tags, newTag])
setNewTag("")
}
}
const handleRemoveTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!content) {
toast({
title: "错误",
description: "请输入素材内容",
variant: "destructive",
})
return
}
try {
// 模拟保存新素材
await new Promise((resolve) => setTimeout(resolve, 1000))
toast({
title: "成功",
description: "新素材已创建",
})
router.push(`/content/${params.id}/materials`)
} catch (error) {
console.error("Failed to create new material:", error)
toast({
title: "错误",
description: "创建新素材失败",
variant: "destructive",
})
}
}
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">
<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 htmlFor="tags"></Label>
<div className="flex items-center mt-1">
<Input
id="tags"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
placeholder="输入标签"
className="flex-1"
/>
<Button type="button" onClick={handleAddTag} className="ml-2">
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{tags.map((tag, index) => (
<Badge key={index} variant="secondary" className="flex items-center">
{tag}
<Button
type="button"
variant="ghost"
size="sm"
className="h-4 w-4 ml-1 p-0"
onClick={() => handleRemoveTag(tag)}
>
<X className="h-3 w-3" />
</Button>
</Badge>
))}
</div>
</div>
<Button type="submit" className="w-full">
</Button>
</form>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,331 @@
"use client"
import { useState, useEffect } from "react"
import { ChevronLeft, Download, Plus, Search, Tag, Trash2, BarChart } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { useRouter } from "next/navigation"
import { toast } from "@/components/ui/use-toast"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { useInView } from "react-intersection-observer"
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu"
interface Material {
id: string
content: string
tags: string[]
aiAnalysis?: string
}
export default function MaterialsPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [materials, setMaterials] = useState<Material[]>([])
const [searchQuery, setSearchQuery] = useState("")
const [isLoading, setIsLoading] = useState(true)
const [selectedMaterial, setSelectedMaterial] = useState<Material | null>(null)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const { ref, inView } = useInView()
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([])
const [exportFormat, setExportFormat] = useState<"excel" | "csv" | "json">("excel")
const [isExporting, setIsExporting] = useState(false)
useEffect(() => {
if (inView && hasMore && !isLoading) {
loadMoreMaterials()
}
}, [inView, hasMore, isLoading])
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()
}, [])
const loadMoreMaterials = async () => {
setIsLoading(true)
try {
// 模拟从API获取更多素材数据
await new Promise((resolve) => setTimeout(resolve, 500))
const newMaterials: Material[] = [
{
id: String(materials.length + 1),
content: `More content ${materials.length + 1}`,
tags: ["more", "content"],
},
]
if (newMaterials.length === 0) {
setHasMore(false)
} else {
setMaterials([...materials, ...newMaterials])
setPage(page + 1)
}
} catch (error) {
console.error("Failed to fetch more materials:", error)
toast({
title: "错误",
description: "获取更多素材数据失败",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
const handleNewMaterial = () => {
// 实现新建素材功能
router.push(`/content/${params.id}/materials/new`)
}
const handleAIAnalysis = async (material: Material) => {
try {
// 模拟AI分析过程
await new Promise((resolve) => setTimeout(resolve, 1000))
const analysis = "这是一条" + material.tags.join("、") + "相关的内容,情感倾向积极。"
setMaterials(materials.map((m) => (m.id === material.id ? { ...m, aiAnalysis: analysis } : m)))
setSelectedMaterial({ ...material, aiAnalysis: analysis })
} catch (error) {
console.error("AI analysis failed:", error)
toast({
title: "错误",
description: "AI分析失败",
variant: "destructive",
})
}
}
const filteredMaterials = materials.filter(
(material) =>
material.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
material.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())),
)
const toggleSelectMaterial = (id: string) => {
if (selectedMaterials.includes(id)) {
setSelectedMaterials(selectedMaterials.filter((materialId) => materialId !== id))
} else {
setSelectedMaterials([...selectedMaterials, id])
}
}
const selectAllMaterials = () => {
if (selectedMaterials.length === filteredMaterials.length) {
setSelectedMaterials([])
} else {
setSelectedMaterials(filteredMaterials.map((material) => material.id))
}
}
const handleExport = async () => {
setIsExporting(true)
try {
// 模拟导出过程
await new Promise((resolve) => setTimeout(resolve, 1500))
let fileName = `素材数据_${new Date().toISOString().split("T")[0]}`
let fileExtension = ""
switch (exportFormat) {
case "excel":
fileName += ".xlsx"
fileExtension = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
break
case "csv":
fileName += ".csv"
fileExtension = "text/csv"
break
case "json":
fileName += ".json"
fileExtension = "application/json"
break
}
toast({
title: "导出成功",
description: `已成功导出为${fileName}`,
})
} catch (error) {
console.error("Export failed:", error)
toast({
title: "导出失败",
description: "导出素材数据失败",
variant: "destructive",
})
} finally {
setIsExporting(false)
}
}
if (isLoading) {
return <div className="flex justify-center items-center h-screen">...</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>
{selectedMaterials.length > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500"> {selectedMaterials.length} </span>
<Button variant="outline" size="sm" onClick={() => setSelectedMaterials([])}>
</Button>
<Button variant="destructive" size="sm">
</Button>
<Button variant="outline" size="sm">
</Button>
</div>
)}
<div className="flex items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" disabled={isExporting}>
{isExporting ? (
<>
<span className="mr-2"></span>
<span className="animate-spin"></span>
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
</>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup value={exportFormat} onValueChange={(value) => setExportFormat(value as any)}>
<DropdownMenuRadioItem value="excel">Excel格式</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="csv">CSV格式</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="json">JSON格式</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleExport}></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button onClick={handleNewMaterial}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</header>
<div className="p-4">
<Card className="p-4">
<div className="space-y-4">
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索素材或标签..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="space-y-2">
{filteredMaterials.map((material) => (
<div key={material.id} className="flex items-center justify-between bg-white p-3 rounded-lg shadow">
<div className="flex-1">
<input
type="checkbox"
checked={selectedMaterials.includes(material.id)}
onChange={() => toggleSelectMaterial(material.id)}
/>
<p className="text-sm text-gray-600 mb-2">{material.content}</p>
<div className="flex flex-wrap gap-2">
{material.tags.map((tag, index) => (
<Badge key={index} variant="secondary">
<Tag className="h-3 w-3 mr-1" />
{tag}
</Badge>
))}
</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>
</div>
</div>
))}
{filteredMaterials.length > 0 && hasMore && (
<div ref={ref} className="py-4 text-center">
{isLoading ? "加载中..." : "向下滚动加载更多"}
</div>
)}
</div>
</div>
</Card>
</div>
</div>
)
}

289
app/content/[id]/page.tsx Normal file
View File

@@ -0,0 +1,289 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Save } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea"
import { DateRangePicker } from "@/components/ui/date-range-picker"
import { WechatFriendSelector } from "@/components/WechatFriendSelector"
import { WechatGroupSelector } from "@/components/WechatGroupSelector"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { toast } from "@/components/ui/use-toast"
interface ContentLibrary {
id: string
name: string
sourceType: "friends" | "groups"
keywordsInclude: string
keywordsExclude: string
startDate: string
endDate: string
selectedFriends: any[]
selectedGroups: any[]
useAI: boolean
aiPrompt: string
enabled: boolean
}
export default function ContentLibraryPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [library, setLibrary] = useState<ContentLibrary | null>(null)
const [isWechatFriendSelectorOpen, setIsWechatFriendSelectorOpen] = useState(false)
const [isWechatGroupSelectorOpen, setIsWechatGroupSelectorOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const fetchLibrary = async () => {
setIsLoading(true)
try {
// 模拟从API获取内容库数据
await new Promise((resolve) => setTimeout(resolve, 500))
const data = {
id: params.id,
name: "示例内容库",
sourceType: "friends",
keywordsInclude: "关键词1,关键词2",
keywordsExclude: "排除词1,排除词2",
startDate: "2024-01-01",
endDate: "2024-12-31",
selectedFriends: [
{ id: "1", nickname: "张三", avatar: "/placeholder.svg?height=40&width=40" },
{ id: "2", nickname: "李四", avatar: "/placeholder.svg?height=40&width=40" },
],
selectedGroups: [],
useAI: true,
aiPrompt: "AI提示词示例",
enabled: true,
}
setLibrary(data)
} catch (error) {
console.error("Failed to fetch library data:", error)
toast({
title: "错误",
description: "获取内容库数据失败",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
fetchLibrary()
}, [params.id])
const handleSave = async () => {
if (!library) return
try {
// 模拟保存到API
await new Promise((resolve) => setTimeout(resolve, 500))
toast({
title: "成功",
description: "内容库已保存",
})
// 这里应该调用一个函数来更新外部展示的数据
// updateExternalDisplay(library)
} catch (error) {
console.error("Failed to save library:", error)
toast({
title: "错误",
description: "保存内容库失败",
variant: "destructive",
})
}
}
if (isLoading) {
return <div className="flex justify-center items-center h-screen">...</div>
}
if (!library) {
return <div className="flex justify-center items-center h-screen"></div>
}
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-16">
<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>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
</header>
<div className="p-4 space-y-4">
<Card className="p-4">
<div className="space-y-4">
<div>
<Label htmlFor="name" className="text-base required">
</Label>
<Input
id="name"
value={library.name}
onChange={(e) => setLibrary({ ...library, name: e.target.value })}
placeholder="请输入内容库名称"
required
className="mt-1.5"
/>
</div>
<div>
<Label className="text-base"></Label>
<Tabs
value={library.sourceType}
onValueChange={(value: "friends" | "groups") => setLibrary({ ...library, sourceType: value })}
className="mt-1.5"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="friends"></TabsTrigger>
<TabsTrigger value="groups"></TabsTrigger>
</TabsList>
<TabsContent value="friends" className="mt-4">
<Button variant="outline" className="w-full" onClick={() => setIsWechatFriendSelectorOpen(true)}>
</Button>
{library.selectedFriends.length > 0 && (
<div className="mt-2 space-y-2">
{library.selectedFriends.map((friend) => (
<div key={friend.id} className="flex items-center justify-between bg-gray-100 p-2 rounded-md">
<span>{friend.nickname}</span>
</div>
))}
</div>
)}
</TabsContent>
<TabsContent value="groups" className="mt-4">
<Button variant="outline" className="w-full" onClick={() => setIsWechatGroupSelectorOpen(true)}>
</Button>
{library.selectedGroups.length > 0 && (
<div className="mt-2 space-y-2">
{library.selectedGroups.map((group) => (
<div key={group.id} className="flex items-center justify-between bg-gray-100 p-2 rounded-md">
<span>{group.name}</span>
</div>
))}
</div>
)}
</TabsContent>
</Tabs>
</div>
<Accordion type="single" collapsible>
<AccordionItem value="keywords">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div>
<Label htmlFor="keywordsInclude" className="text-base">
</Label>
<Textarea
id="keywordsInclude"
value={library.keywordsInclude}
onChange={(e) => setLibrary({ ...library, keywordsInclude: e.target.value })}
placeholder="如果设置了关键字,系统只会采集含有关键字的内容。多个关键字,用半角的','隔开。"
className="mt-1.5 min-h-[100px]"
/>
</div>
<div>
<Label htmlFor="keywordsExclude" className="text-base">
</Label>
<Textarea
id="keywordsExclude"
value={library.keywordsExclude}
onChange={(e) => setLibrary({ ...library, keywordsExclude: e.target.value })}
placeholder="如果设置了关键字,匹配到关键字的,系统将不会采集。多个关键字,用半角的','隔开。"
className="mt-1.5 min-h-[100px]"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex items-center justify-between">
<div>
<Label className="text-base">AI</Label>
<p className="text-sm text-gray-500 mt-1">
AI之后AI重新生成内容
</p>
</div>
<Switch
checked={library.useAI}
onCheckedChange={(checked) => setLibrary({ ...library, useAI: checked })}
/>
</div>
{library.useAI && (
<div>
<Label htmlFor="aiPrompt" className="text-base">
AI
</Label>
<Textarea
id="aiPrompt"
value={library.aiPrompt}
onChange={(e) => setLibrary({ ...library, aiPrompt: e.target.value })}
placeholder="请输入 AI 提示词"
className="mt-1.5 min-h-[100px]"
/>
</div>
)}
<div>
<Label className="text-base"></Label>
<DateRangePicker
className="mt-1.5"
onChange={(range) => {
if (range?.from) {
setLibrary({
...library,
startDate: range.from.toISOString(),
endDate: range.to?.toISOString() || "",
})
}
}}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-base required"></Label>
<Switch
checked={library.enabled}
onCheckedChange={(checked) => setLibrary({ ...library, enabled: checked })}
/>
</div>
</div>
</Card>
</div>
<WechatFriendSelector
open={isWechatFriendSelectorOpen}
onOpenChange={setIsWechatFriendSelectorOpen}
selectedFriends={library.selectedFriends}
onSelect={(friends) => setLibrary({ ...library, selectedFriends: friends })}
/>
<WechatGroupSelector
open={isWechatGroupSelectorOpen}
onOpenChange={setIsWechatGroupSelectorOpen}
selectedGroups={library.selectedGroups}
onSelect={(groups) => setLibrary({ ...library, selectedGroups: groups })}
/>
</div>
)
}

52
app/content/loading.tsx Normal file
View File

@@ -0,0 +1,52 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export default function ContentLoading() {
return (
<div className="container mx-auto p-4 space-y-6">
<div className="flex items-center justify-between mb-6">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-10 w-32" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
{Array(3)
.fill(0)
.map((_, i) => (
<Card key={i} className="shadow-sm">
<CardHeader className="pb-2">
<Skeleton className="h-6 w-32 mb-2" />
</CardHeader>
<CardContent>
<Skeleton className="h-16 w-full" />
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>
<Skeleton className="h-7 w-48" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{Array(5)
.fill(0)
.map((_, i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<Skeleton className="h-8 w-24" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,116 @@
"use client"
import { useState } from "react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Search, Plus } from "lucide-react"
interface Device {
id: string
name: string
account: string
status: "online" | "offline"
}
const mockDevices: Device[] = [
{
id: "1",
name: "iPhone 13 Pro",
account: "wxid_abc123",
status: "online",
},
{
id: "2",
name: "Huawei P40",
account: "wxid_xyz789",
status: "offline",
},
]
interface DeviceSelectorProps {
selectedDevices: string[]
onChange: (devices: string[]) => void
}
export function DeviceSelector({ selectedDevices, onChange }: DeviceSelectorProps) {
const [searchQuery, setSearchQuery] = useState("")
const toggleSelectAll = () => {
if (selectedDevices.length === mockDevices.length) {
onChange([])
} else {
onChange(mockDevices.map((device) => device.id))
}
}
const toggleDevice = (deviceId: string) => {
if (selectedDevices.includes(deviceId)) {
onChange(selectedDevices.filter((id) => id !== deviceId))
} else {
onChange([...selectedDevices, deviceId])
}
}
return (
<Card className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索设备"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Button className="ml-2" size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox checked={selectedDevices.length === mockDevices.length} onCheckedChange={toggleSelectAll} />
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockDevices.map((device) => (
<TableRow key={device.id}>
<TableCell>
<Checkbox
checked={selectedDevices.includes(device.id)}
onCheckedChange={() => toggleDevice(device.id)}
/>
</TableCell>
<TableCell>{device.name}</TableCell>
<TableCell>{device.account}</TableCell>
<TableCell>
<span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs ${
device.status === "online" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
}`}
>
{device.status === "online" ? "在线" : "离线"}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</Card>
)
}

273
app/content/new/page.tsx Normal file
View File

@@ -0,0 +1,273 @@
"use client"
import { useState } from "react"
import { ChevronLeft, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea"
import { useRouter } from "next/navigation"
import { DateRangePicker } from "@/components/ui/date-range-picker"
import { WechatFriendSelector } from "@/components/WechatFriendSelector"
import { WechatGroupSelector } from "@/components/WechatGroupSelector"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
interface WechatFriend {
id: string
nickname: string
wechatId: string
avatar: string
gender: "male" | "female"
customer: string
}
interface WechatGroup {
id: string
name: string
memberCount: number
avatar: string
owner: string
customer: string
}
export default function NewContentLibraryPage() {
const router = useRouter()
const [formData, setFormData] = useState({
name: "",
sourceType: "friends" as "friends" | "groups",
keywordsInclude: "",
keywordsExclude: "",
startDate: "",
endDate: "",
selectedFriends: [] as WechatFriend[],
selectedGroups: [] as WechatGroup[],
useAI: false,
aiPrompt: "",
enabled: true,
})
const [isWechatFriendSelectorOpen, setIsWechatFriendSelectorOpen] = useState(false)
const [isWechatGroupSelectorOpen, setIsWechatGroupSelectorOpen] = useState(false)
const removeFriend = (friendId: string) => {
setFormData((prev) => ({
...prev,
selectedFriends: prev.selectedFriends.filter((friend) => friend.id !== friendId),
}))
}
const removeGroup = (groupId: string) => {
setFormData((prev) => ({
...prev,
selectedGroups: prev.selectedGroups.filter((group) => group.id !== groupId),
}))
}
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-16">
<header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center p-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="ml-2 text-lg font-medium"></h1>
</div>
</header>
<div className="p-4 space-y-4">
<Card className="p-4">
<div className="space-y-4">
<div>
<Label htmlFor="name" className="text-base required">
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="请输入内容库名称"
required
className="mt-1.5"
/>
</div>
<div>
<Label className="text-base"></Label>
<Tabs
value={formData.sourceType}
onValueChange={(value: "friends" | "groups") => setFormData({ ...formData, sourceType: value })}
className="mt-1.5"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="friends"></TabsTrigger>
<TabsTrigger value="groups"></TabsTrigger>
</TabsList>
<TabsContent value="friends" className="mt-4">
<Button variant="outline" className="w-full" onClick={() => setIsWechatFriendSelectorOpen(true)}>
</Button>
{formData.selectedFriends.length > 0 && (
<div className="mt-2 space-y-2">
{formData.selectedFriends.map((friend) => (
<div key={friend.id} className="flex items-center justify-between bg-gray-100 p-2 rounded-md">
<div className="flex items-center space-x-2">
<img
src={friend.avatar || "/placeholder.svg"}
alt={friend.nickname}
className="w-8 h-8 rounded-full"
/>
<span>{friend.nickname}</span>
</div>
<Button variant="ghost" size="sm" onClick={() => removeFriend(friend.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</TabsContent>
<TabsContent value="groups" className="mt-4">
<Button variant="outline" className="w-full" onClick={() => setIsWechatGroupSelectorOpen(true)}>
</Button>
{formData.selectedGroups.length > 0 && (
<div className="mt-2 space-y-2">
{formData.selectedGroups.map((group) => (
<div key={group.id} className="flex items-center justify-between bg-gray-100 p-2 rounded-md">
<div className="flex items-center space-x-2">
<img
src={group.avatar || "/placeholder.svg"}
alt={group.name}
className="w-8 h-8 rounded-full"
/>
<span>{group.name}</span>
</div>
<Button variant="ghost" size="sm" onClick={() => removeGroup(group.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</TabsContent>
</Tabs>
</div>
<Accordion type="single" collapsible>
<AccordionItem value="keywords">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div>
<Label htmlFor="keywordsInclude" className="text-base">
</Label>
<Textarea
id="keywordsInclude"
value={formData.keywordsInclude}
onChange={(e) => setFormData({ ...formData, keywordsInclude: e.target.value })}
placeholder="如果设置了关键字,系统只会采集含有关键字的内容。多个关键字,用半角的','隔开。"
className="mt-1.5 min-h-[100px]"
/>
</div>
<div>
<Label htmlFor="keywordsExclude" className="text-base">
</Label>
<Textarea
id="keywordsExclude"
value={formData.keywordsExclude}
onChange={(e) => setFormData({ ...formData, keywordsExclude: e.target.value })}
placeholder="如果设置了关键字,匹配到关键字的,系统将不会采集。多个关键字,用半角的','隔开。"
className="mt-1.5 min-h-[100px]"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex items-center justify-between">
<div>
<Label className="text-base">AI</Label>
<p className="text-sm text-gray-500 mt-1">
AI之后AI重新生成内容
</p>
</div>
<Switch
checked={formData.useAI}
onCheckedChange={(checked) => setFormData({ ...formData, useAI: checked })}
/>
</div>
{formData.useAI && (
<div>
<Label htmlFor="aiPrompt" className="text-base">
AI
</Label>
<Textarea
id="aiPrompt"
value={formData.aiPrompt}
onChange={(e) => setFormData({ ...formData, aiPrompt: e.target.value })}
placeholder="请输入 AI 提示词"
className="mt-1.5 min-h-[100px]"
/>
</div>
)}
<div>
<Label className="text-base"></Label>
<DateRangePicker
className="mt-1.5"
onChange={(range) => {
if (range?.from) {
setFormData({
...formData,
startDate: range.from.toISOString(),
endDate: range.to?.toISOString() || "",
})
}
}}
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-base required"></Label>
<Switch
checked={formData.enabled}
onCheckedChange={(checked) => setFormData({ ...formData, enabled: checked })}
/>
</div>
</div>
</Card>
<div className="flex gap-4">
<Button type="button" variant="outline" className="flex-1" onClick={() => router.back()}>
</Button>
<Button type="submit" className="flex-1">
</Button>
</div>
</div>
<WechatFriendSelector
open={isWechatFriendSelectorOpen}
onOpenChange={setIsWechatFriendSelectorOpen}
selectedFriends={formData.selectedFriends}
onSelect={(friends) => setFormData({ ...formData, selectedFriends: friends })}
/>
<WechatGroupSelector
open={isWechatGroupSelectorOpen}
onOpenChange={setIsWechatGroupSelectorOpen}
selectedGroups={formData.selectedGroups}
onSelect={(groups) => setFormData({ ...formData, selectedGroups: groups })}
/>
</div>
)
}

213
app/content/page.tsx Normal file
View File

@@ -0,0 +1,213 @@
"use client"
import { useState } from "react"
import { ChevronLeft, Filter, Search, RefreshCw, Plus, Edit, Trash2, Eye, MoreVertical } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { useRouter } from "next/navigation"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import Image from "next/image"
interface ContentLibrary {
id: string
name: string
source: "friends" | "groups"
targetAudience: {
id: string
nickname: string
avatar: string
}[]
creator: string
itemCount: number
lastUpdated: string
enabled: boolean
}
export default function ContentLibraryPage() {
const router = useRouter()
const [libraries, setLibraries] = useState<ContentLibrary[]>([
{
id: "129",
name: "微信好友广告",
source: "friends",
targetAudience: [
{ id: "1", nickname: "张三", avatar: "/placeholder.svg?height=40&width=40" },
{ id: "2", nickname: "李四", avatar: "/placeholder.svg?height=40&width=40" },
{ id: "3", nickname: "王五", avatar: "/placeholder.svg?height=40&width=40" },
],
creator: "海尼",
itemCount: 0,
lastUpdated: "2024-02-09 12:30",
enabled: false,
},
{
id: "127",
name: "开发群",
source: "groups",
targetAudience: [{ id: "4", nickname: "开发群1", avatar: "/placeholder.svg?height=40&width=40" }],
creator: "karuo",
itemCount: 0,
lastUpdated: "2024-02-09 12:30",
enabled: true,
},
])
const [searchQuery, setSearchQuery] = useState("")
const [activeTab, setActiveTab] = useState("all")
const handleCreateNew = () => {
// 模拟创建新内容库
const newId = Date.now().toString()
const newLibrary = {
id: newId,
name: "新内容库",
source: "friends" as const,
targetAudience: [],
creator: "当前用户",
itemCount: 0,
lastUpdated: new Date().toISOString(),
enabled: true,
}
setLibraries([newLibrary, ...libraries])
router.push(`/content/${newId}`)
}
const handleEdit = (id: string) => {
router.push(`/content/${id}`)
}
const handleDelete = (id: string) => {
// 实现删除功能
setLibraries(libraries.filter((lib) => lib.id !== id))
}
const handleViewMaterials = (id: string) => {
router.push(`/content/${id}/materials`)
}
const filteredLibraries = libraries.filter(
(library) =>
(activeTab === "all" || library.source === activeTab) &&
(library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
library.targetAudience.some((target) => target.nickname.toLowerCase().includes(searchQuery.toLowerCase()))),
)
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>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</header>
<div className="p-4 space-y-4">
<Card className="p-4">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索内容库..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="all"></TabsTrigger>
<TabsTrigger value="friends"></TabsTrigger>
<TabsTrigger value="groups"></TabsTrigger>
</TabsList>
</Tabs>
<div className="space-y-3">
{filteredLibraries.map((library) => (
<Card key={library.id} className="p-4">
<div className="flex items-start justify-between">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<h3 className="font-medium">{library.name}</h3>
<Badge variant={library.enabled ? "success" : "secondary"}>
{library.enabled ? "已启用" : "已停用"}
</Badge>
</div>
<div className="text-sm text-gray-500">
<div className="flex items-center space-x-1">
<span></span>
<div className="flex -space-x-2 overflow-hidden">
{library.targetAudience.slice(0, 3).map((target) => (
<Image
key={target.id}
src={target.avatar || "/placeholder.svg"}
alt={target.nickname}
width={24}
height={24}
className="inline-block h-6 w-6 rounded-full ring-2 ring-white"
/>
))}
{library.targetAudience.length > 3 && (
<span className="flex items-center justify-center w-6 h-6 text-xs font-medium text-white bg-gray-400 rounded-full ring-2 ring-white">
+{library.targetAudience.length - 3}
</span>
)}
</div>
</div>
<div>{library.creator}</div>
<div>{library.itemCount}</div>
<div>{library.lastUpdated}</div>
</div>
</div>
<div className="flex space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEdit(library.id)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(library.id)}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleViewMaterials(library.id)}>
<Eye className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</Card>
))}
</div>
</div>
</Card>
</div>
</div>
)
}