feat: 画像特征支持增删项目
- 画像特征区域右上角加"添加"按钮,弹窗输入名称和值 - 每个特征hover显示删除图标,点击删除 - 无特征时显示"添加画像特征"入口 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { UserCircle, History, ChevronRight, ArrowLeft, ChevronDown, Pencil } from 'lucide-react'
|
import { UserCircle, History, ChevronRight, ArrowLeft, ChevronDown, Pencil, Plus, Trash2 } from 'lucide-react'
|
||||||
import { PageLayout } from '@/components/common/PageLayout'
|
import { PageLayout } from '@/components/common/PageLayout'
|
||||||
import { EmptyState } from '@/components/common/EmptyState'
|
import { EmptyState } from '@/components/common/EmptyState'
|
||||||
import { LoadingState } from '@/components/common/LoadingState'
|
import { LoadingState } from '@/components/common/LoadingState'
|
||||||
@@ -432,6 +432,9 @@ export function MyProfile() {
|
|||||||
const [editing, setEditing] = useState<{ field: string; label: string; value: string; type: 'select' | 'text' } | null>(null)
|
const [editing, setEditing] = useState<{ field: string; label: string; value: string; type: 'select' | 'text' } | null>(null)
|
||||||
const [editValue, setEditValue] = useState('')
|
const [editValue, setEditValue] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [addingFact, setAddingFact] = useState(false)
|
||||||
|
const [newFactKey, setNewFactKey] = useState('')
|
||||||
|
const [newFactValue, setNewFactValue] = useState('')
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -492,6 +495,39 @@ export function MyProfile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除画像特征 */
|
||||||
|
const handleDeleteFact = async (key: string) => {
|
||||||
|
if (!profile) return
|
||||||
|
try {
|
||||||
|
// 通过更新空值来删除
|
||||||
|
const newFacts = { ...profile.key_facts }
|
||||||
|
delete newFacts[key]
|
||||||
|
await api.updateProfile({ key_facts: newFacts })
|
||||||
|
setProfile((prev) => prev ? { ...prev, key_facts: newFacts } : prev)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加画像特征 */
|
||||||
|
const handleAddFact = async () => {
|
||||||
|
if (!newFactKey.trim() || !newFactValue.trim() || !profile) return
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const key = newFactKey.trim()
|
||||||
|
const newFacts = { ...profile.key_facts, [key]: newFactValue.trim() }
|
||||||
|
await api.updateProfile({ key_facts: newFacts })
|
||||||
|
setProfile((prev) => prev ? { ...prev, key_facts: newFacts } : prev)
|
||||||
|
setAddingFact(false)
|
||||||
|
setNewFactKey('')
|
||||||
|
setNewFactValue('')
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : '添加失败')
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <PageLayout title="我的画像"><LoadingState /></PageLayout>
|
if (loading) return <PageLayout title="我的画像"><LoadingState /></PageLayout>
|
||||||
if (error) return <PageLayout title="我的画像"><ErrorState message={error} onRetry={load} /></PageLayout>
|
if (error) return <PageLayout title="我的画像"><ErrorState message={error} onRetry={load} /></PageLayout>
|
||||||
if (!profile) return null
|
if (!profile) return null
|
||||||
@@ -625,10 +661,18 @@ export function MyProfile() {
|
|||||||
const caseFacts = entries.filter(([k]) => !isProfileFact(k))
|
const caseFacts = entries.filter(([k]) => !isProfileFact(k))
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 画像特征:直接展示(可编辑) */}
|
{/* 画像特征:直接展示(可编辑/可增删) */}
|
||||||
{profileFacts.length > 0 && (
|
{profileFacts.length > 0 && (
|
||||||
<div className="border-b pb-2">
|
<div className="border-b pb-2">
|
||||||
<p className="mb-1 text-xs text-muted-foreground">画像特征</p>
|
<div className="mb-1 flex items-center justify-between">
|
||||||
|
<p className="text-xs text-muted-foreground">画像特征</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setAddingFact(true)}
|
||||||
|
className="flex items-center gap-0.5 text-xs text-primary hover:text-primary/80"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3">
|
||||||
{profileFacts.map(([k, v]) => (
|
{profileFacts.map(([k, v]) => (
|
||||||
<div key={k} className="group flex items-center justify-between text-xs">
|
<div key={k} className="group flex items-center justify-between text-xs">
|
||||||
@@ -642,12 +686,30 @@ export function MyProfile() {
|
|||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3 text-muted-foreground hover:text-primary" />
|
<Pencil className="h-3 w-3 text-muted-foreground hover:text-primary" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteFact(k)}
|
||||||
|
className="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
aria-label={`删除${translateFactKey(k)}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3 text-muted-foreground hover:text-red-500" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* 无画像特征时也显示添加按钮 */}
|
||||||
|
{profileFacts.length === 0 && (
|
||||||
|
<div className="border-b pb-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setAddingFact(true)}
|
||||||
|
className="flex items-center gap-0.5 text-xs text-primary hover:text-primary/80"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />添加画像特征
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 个案事实:折叠展示 */}
|
{/* 个案事实:折叠展示 */}
|
||||||
{caseFacts.length > 0 && (
|
{caseFacts.length > 0 && (
|
||||||
<div className="border-b pb-2">
|
<div className="border-b pb-2">
|
||||||
@@ -778,6 +840,41 @@ export function MyProfile() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 添加画像特征弹窗 */}
|
||||||
|
<Dialog open={addingFact} onOpenChange={setAddingFact}>
|
||||||
|
<DialogContent className="max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>添加画像特征</DialogTitle>
|
||||||
|
<DialogDescription>填写特征名称和值,保存后生效</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs text-muted-foreground">特征名称</label>
|
||||||
|
<Input
|
||||||
|
value={newFactKey}
|
||||||
|
onChange={(e) => setNewFactKey(e.target.value)}
|
||||||
|
placeholder="如:行业、公司规模、地区"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs text-muted-foreground">特征值</label>
|
||||||
|
<Input
|
||||||
|
value={newFactValue}
|
||||||
|
onChange={(e) => setNewFactValue(e.target.value)}
|
||||||
|
placeholder="如:科技、小型、上海"
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleAddFact()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setAddingFact(false)}>取消</Button>
|
||||||
|
<Button onClick={handleAddFact} disabled={saving || !newFactKey.trim() || !newFactValue.trim()}>
|
||||||
|
{saving ? '保存中...' : '添加'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user