存客宝 React
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { X, Plus } from "lucide-react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
interface AnalysisSettingsProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function AnalysisSettings({ formData, updateFormData, onNext, onBack }: AnalysisSettingsProps) {
|
||||
const [analysisType, setAnalysisType] = useState<string>(formData.analysisType || "both")
|
||||
const [keyword, setKeyword] = useState("")
|
||||
const [promptWord, setPromptWord] = useState("")
|
||||
const [keywords, setKeywords] = useState<string[]>(formData.keywords || [])
|
||||
const [promptWords, setPromptWords] = useState<string[]>(formData.promptWords || [])
|
||||
const [activeTab, setActiveTab] = useState("friends")
|
||||
|
||||
const handleAddKeyword = () => {
|
||||
if (keyword.trim() && !keywords.includes(keyword.trim())) {
|
||||
const newKeywords = [...keywords, keyword.trim()]
|
||||
setKeywords(newKeywords)
|
||||
updateFormData({ keywords: newKeywords })
|
||||
setKeyword("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveKeyword = (index: number) => {
|
||||
const newKeywords = keywords.filter((_, i) => i !== index)
|
||||
setKeywords(newKeywords)
|
||||
updateFormData({ keywords: newKeywords })
|
||||
}
|
||||
|
||||
const handleAddPromptWord = () => {
|
||||
if (promptWord.trim() && !promptWords.includes(promptWord.trim())) {
|
||||
const newPromptWords = [...promptWords, promptWord.trim()]
|
||||
setPromptWords(newPromptWords)
|
||||
updateFormData({ promptWords: newPromptWords })
|
||||
setPromptWord("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemovePromptWord = (index: number) => {
|
||||
const newPromptWords = promptWords.filter((_, i) => i !== index)
|
||||
setPromptWords(newPromptWords)
|
||||
updateFormData({ promptWords: newPromptWords })
|
||||
}
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
setAnalysisType(value)
|
||||
updateFormData({ analysisType: value })
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, type: "keyword" | "prompt") => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
if (type === "keyword") {
|
||||
handleAddKeyword()
|
||||
} else {
|
||||
handleAddPromptWord()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
updateFormData({
|
||||
analysisType,
|
||||
keywords,
|
||||
promptWords,
|
||||
})
|
||||
onNext()
|
||||
}
|
||||
|
||||
const suggestedKeywords = ["美妆", "护肤", "彩妆", "健身", "教育", "培训", "金融", "投资", "旅游", "美食"]
|
||||
const suggestedPrompts = ["人群属性", "喜好分析", "消费能力", "兴趣爱好", "活跃度", "社交特征"]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">设置分析内容</h2>
|
||||
<p className="text-gray-500 text-sm">选择分析类型并设置关键词和提示词</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base font-medium">分析类型</Label>
|
||||
<RadioGroup value={analysisType} onValueChange={handleTypeChange} className="mt-2 space-y-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="friends" id="friends" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="friends" className="font-medium">
|
||||
好友信息分析
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">分析微信号下所有好友的昵称及微信信息,基于指定用户词进行筛选分析</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="moments" id="moments" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="moments" className="font-medium">
|
||||
朋友圈内容分析
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">
|
||||
运用AI技术对所有用户朋友圈内容进行分析,通过提示词辅助深入剖析用户类型
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="both" id="both" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="both" className="font-medium">
|
||||
综合分析(推荐)
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">同时分析好友信息和朋友圈内容,获得更全面的数据洞察</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="friends">好友分析设置</TabsTrigger>
|
||||
<TabsTrigger value="moments">朋友圈分析设置</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="friends" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label htmlFor="keywords" className="text-base font-medium">
|
||||
用户关键词
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1 mb-2">
|
||||
设置用于筛选好友的关键词,例如"美妆"将筛选出昵称或微信信息中与美妆相关的好友
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="keywords"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, "keyword")}
|
||||
placeholder="输入关键词并回车"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="button" onClick={handleAddKeyword} disabled={!keyword.trim()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{keywords.map((kw, index) => (
|
||||
<Badge key={index} variant="secondary" className="pl-2 pr-1 py-1 flex items-center gap-1">
|
||||
{kw}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveKeyword(index)}
|
||||
className="ml-1 rounded-full hover:bg-gray-200 p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3">
|
||||
<p className="text-sm text-gray-500 mb-2">推荐关键词:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestedKeywords.map((kw) => (
|
||||
<Badge
|
||||
key={kw}
|
||||
variant="outline"
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
if (!keywords.includes(kw)) {
|
||||
const newKeywords = [...keywords, kw]
|
||||
setKeywords(newKeywords)
|
||||
updateFormData({ keywords: newKeywords })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{kw}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="moments" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label htmlFor="promptWords" className="text-base font-medium">
|
||||
分析提示词
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1 mb-2">
|
||||
设置辅助AI分析的提示词,如"人群属性"、"喜好分析"等,帮助深入剖析用户类型
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="promptWords"
|
||||
value={promptWord}
|
||||
onChange={(e) => setPromptWord(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, "prompt")}
|
||||
placeholder="输入提示词并回车"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="button" onClick={handleAddPromptWord} disabled={!promptWord.trim()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{promptWords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{promptWords.map((pw, index) => (
|
||||
<Badge key={index} variant="secondary" className="pl-2 pr-1 py-1 flex items-center gap-1">
|
||||
{pw}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemovePromptWord(index)}
|
||||
className="ml-1 rounded-full hover:bg-gray-200 p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3">
|
||||
<p className="text-sm text-gray-500 mb-2">推荐提示词:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestedPrompts.map((pw) => (
|
||||
<Badge
|
||||
key={pw}
|
||||
variant="outline"
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
if (!promptWords.includes(pw)) {
|
||||
const newPromptWords = [...promptWords, pw]
|
||||
setPromptWords(newPromptWords)
|
||||
updateFormData({ promptWords: newPromptWords })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pw}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={
|
||||
((analysisType === "friends" || analysisType === "both") && keywords.length === 0) ||
|
||||
((analysisType === "moments" || analysisType === "both") && promptWords.length === 0)
|
||||
}
|
||||
>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Smartphone, User, MessageCircle, Mail } from "lucide-react"
|
||||
|
||||
interface ConfirmAnalysisProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function ConfirmAnalysis({ formData, updateFormData, onSubmit, onBack }: ConfirmAnalysisProps) {
|
||||
const [planName, setPlanName] = useState(formData.name || "")
|
||||
const [emailReport, setEmailReport] = useState(formData.emailReport || false)
|
||||
const [email, setEmail] = useState(formData.email || "")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const getAnalysisTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "friends":
|
||||
return "好友信息分析"
|
||||
case "moments":
|
||||
return "朋友圈内容分析"
|
||||
case "both":
|
||||
return "综合分析"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
updateFormData({
|
||||
name: planName,
|
||||
emailReport,
|
||||
email: emailReport ? email : "",
|
||||
})
|
||||
|
||||
setIsSubmitting(true)
|
||||
await onSubmit()
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">确认分析计划</h2>
|
||||
<p className="text-gray-500 text-sm">确认分析计划信息并提交</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="planName" className="text-base font-medium">
|
||||
计划名称
|
||||
</Label>
|
||||
<Input
|
||||
id="planName"
|
||||
value={planName}
|
||||
onChange={(e) => setPlanName(e.target.value)}
|
||||
placeholder="输入分析计划名称"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-4">
|
||||
<h3 className="font-medium">分析计划信息</h3>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<Smartphone className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">设备信息</p>
|
||||
<p className="text-sm text-gray-600">{formData.deviceName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<User className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">微信账号</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{formData.wechatName} ({formData.wechatId})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">分析类型</p>
|
||||
<p className="text-sm text-gray-600">{getAnalysisTypeLabel(formData.analysisType)}</p>
|
||||
|
||||
{(formData.analysisType === "friends" || formData.analysisType === "both") &&
|
||||
formData.keywords.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm font-medium">用户关键词:</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{formData.keywords.map((keyword: string, index: number) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(formData.analysisType === "moments" || formData.analysisType === "both") &&
|
||||
formData.promptWords.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm font-medium">分析提示词:</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{formData.promptWords.map((promptWord: string, index: number) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{promptWord}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="emailReport"
|
||||
checked={emailReport}
|
||||
onCheckedChange={(checked) => {
|
||||
setEmailReport(checked as boolean)
|
||||
if (!checked) {
|
||||
setEmail("")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="emailReport" className="font-medium cursor-pointer">
|
||||
分析完成后发送报告到邮箱
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{emailReport && (
|
||||
<div className="pl-6">
|
||||
<Label htmlFor="email" className="text-sm">
|
||||
邮箱地址
|
||||
</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Mail className="h-4 w-4 text-gray-400 mr-2" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="输入接收报告的邮箱地址"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!planName.trim() || (emailReport && !email.trim()) || isSubmitting}>
|
||||
{isSubmitting ? "提交中..." : "开始分析"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Search, Smartphone } from "lucide-react"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
name: string
|
||||
status: "online" | "offline"
|
||||
wechatAccounts: {
|
||||
id: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
}[]
|
||||
}
|
||||
|
||||
interface DeviceSelectionProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export function DeviceSelection({ formData, updateFormData, onNext }: DeviceSelectionProps) {
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedDevice, setSelectedDevice] = useState<string>(formData.deviceId || "")
|
||||
const [selectedWechatAccount, setSelectedWechatAccount] = useState<string>(formData.wechatId || "")
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载设备数据
|
||||
const fetchDevices = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const mockDevices: Device[] = [
|
||||
{
|
||||
id: "device-1",
|
||||
name: "iPhone 13",
|
||||
status: "online",
|
||||
wechatAccounts: [
|
||||
{ id: "wx-1", nickname: "张小明", wechatId: "wxid_zhang123" },
|
||||
{ id: "wx-2", nickname: "李业务", wechatId: "wxid_libiz456" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "device-2",
|
||||
name: "华为 P40",
|
||||
status: "online",
|
||||
wechatAccounts: [{ id: "wx-3", nickname: "王经理", wechatId: "wxid_wang789" }],
|
||||
},
|
||||
{
|
||||
id: "device-3",
|
||||
name: "小米 12",
|
||||
status: "offline",
|
||||
wechatAccounts: [
|
||||
{ id: "wx-4", nickname: "赵销售", wechatId: "wxid_zhao321" },
|
||||
{ id: "wx-5", nickname: "陈顾问", wechatId: "wxid_chen654" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
setDevices(mockDevices)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchDevices()
|
||||
}, [])
|
||||
|
||||
const filteredDevices = devices.filter(
|
||||
(device) =>
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.wechatAccounts.some(
|
||||
(account) =>
|
||||
account.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
account.wechatId.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
),
|
||||
)
|
||||
|
||||
const handleDeviceSelect = (deviceId: string) => {
|
||||
setSelectedDevice(deviceId)
|
||||
setSelectedWechatAccount("") // 重置微信账号选择
|
||||
|
||||
const selectedDeviceData = devices.find((d) => d.id === deviceId)
|
||||
if (selectedDeviceData) {
|
||||
updateFormData({
|
||||
deviceId,
|
||||
deviceName: selectedDeviceData.name,
|
||||
wechatId: "",
|
||||
wechatName: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleWechatSelect = (wechatId: string) => {
|
||||
setSelectedWechatAccount(wechatId)
|
||||
|
||||
const selectedDeviceData = devices.find((d) => d.id === selectedDevice)
|
||||
if (selectedDeviceData) {
|
||||
const selectedWechatData = selectedDeviceData.wechatAccounts.find((w) => w.id === wechatId)
|
||||
if (selectedWechatData) {
|
||||
updateFormData({
|
||||
wechatId: wechatId,
|
||||
wechatName: selectedWechatData.nickname,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (selectedDevice && selectedWechatAccount) {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">选择设备与微信号</h2>
|
||||
<p className="text-gray-500 text-sm">选择要进行AI数据分析的设备和微信账号</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-base font-medium">1. 选择设备</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-2">
|
||||
{filteredDevices.map((device) => (
|
||||
<Card
|
||||
key={device.id}
|
||||
className={`p-3 cursor-pointer hover:shadow-md transition-shadow ${
|
||||
selectedDevice === device.id ? "ring-2 ring-blue-500" : ""
|
||||
} ${device.status === "offline" ? "opacity-60" : ""}`}
|
||||
onClick={() => device.status === "online" && handleDeviceSelect(device.id)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mr-3">
|
||||
<Smartphone className="h-5 w-5 text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{device.name}</div>
|
||||
<div className="text-sm text-gray-500">{device.wechatAccounts.length} 个微信账号</div>
|
||||
</div>
|
||||
<div
|
||||
className={`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" ? "在线" : "离线"}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{filteredDevices.length === 0 && (
|
||||
<div className="col-span-2 text-center py-8 bg-gray-50 rounded-lg border">
|
||||
<p className="text-gray-500">未找到匹配的设备</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedDevice && (
|
||||
<div>
|
||||
<Label className="text-base font-medium">2. 选择微信账号</Label>
|
||||
<RadioGroup value={selectedWechatAccount} onValueChange={handleWechatSelect} className="mt-2 space-y-2">
|
||||
{devices
|
||||
.find((d) => d.id === selectedDevice)
|
||||
?.wechatAccounts.map((account) => (
|
||||
<div key={account.id} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={account.id} id={account.id} />
|
||||
<Label htmlFor={account.id} className="flex items-center cursor-pointer">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center mr-2">
|
||||
<span className="text-sm font-medium">{account.nickname[0]}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{account.nickname}</div>
|
||||
<div className="text-xs text-gray-500">{account.wechatId}</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={handleContinue} disabled={!selectedDevice || !selectedWechatAccount}>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react"
|
||||
|
||||
interface Step {
|
||||
number: number
|
||||
title: string
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
currentStep: number
|
||||
steps: Step[]
|
||||
}
|
||||
|
||||
export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const isActive = currentStep >= step.number
|
||||
const isLast = index === steps.length - 1
|
||||
|
||||
return (
|
||||
<React.Fragment key={step.number}>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
isActive ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{step.number}
|
||||
</div>
|
||||
<span className={`ml-2 text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500"}`}>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div className={`flex-1 h-0.5 mx-4 ${currentStep > step.number ? "bg-blue-600" : "bg-gray-200"}`} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user