feat: 画像特征支持行内编辑
- 标准字段(行业/规模/角色/风险偏好/沟通偏好/决策风格)hover显示编辑图标 - 画像特征(地区/公司类型/团队等)同样可编辑 - 标准字段用下拉选择,画像特征用文本输入 - 保存后调用 PUT /profile/me 更新,生成新版本快照 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 } from 'lucide-react'
|
import { UserCircle, History, ChevronRight, ArrowLeft, ChevronDown, Pencil } 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'
|
||||||
@@ -7,6 +7,11 @@ import { ErrorState } from '@/components/common/ErrorState'
|
|||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { cn, formatTime } from '@/lib/utils'
|
import { cn, formatTime } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -424,6 +429,9 @@ export function MyProfile() {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [currentHistory, setCurrentHistory] = useState<ProfileHistory | null>(null)
|
const [currentHistory, setCurrentHistory] = useState<ProfileHistory | null>(null)
|
||||||
const [showCaseFacts, setShowCaseFacts] = useState(false)
|
const [showCaseFacts, setShowCaseFacts] = useState(false)
|
||||||
|
const [editing, setEditing] = useState<{ field: string; label: string; value: string; type: 'select' | 'text' } | null>(null)
|
||||||
|
const [editValue, setEditValue] = useState('')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -444,6 +452,46 @@ export function MyProfile() {
|
|||||||
|
|
||||||
useEffect(() => { load() }, [load])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
|
/** 标准字段选项(下拉选择) */
|
||||||
|
const FIELD_OPTIONS: Record<string, { value: string; label: string }[]> = {
|
||||||
|
industry: Object.entries(INDUSTRY_LABELS).map(([v, l]) => ({ value: v, label: l })),
|
||||||
|
company_size: Object.entries(SIZE_LABELS).map(([v, l]) => ({ value: v, label: l })),
|
||||||
|
role: Object.entries(ROLE_LABELS).map(([v, l]) => ({ value: v, label: l })),
|
||||||
|
risk_tolerance: Object.entries(RISK_LABELS).map(([v, l]) => ({ value: v, label: l })),
|
||||||
|
communication_style: Object.entries(COMMUNICATION_STYLE_LABELS).map(([v, l]) => ({ value: v, label: l })),
|
||||||
|
decision_style: Object.entries(DECISION_STYLE_LABELS).map(([v, l]) => ({ value: v, label: l })),
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开编辑 */
|
||||||
|
const handleEdit = (field: string, label: string, value: string) => {
|
||||||
|
const isSelect = !!FIELD_OPTIONS[field]
|
||||||
|
setEditing({ field, label, value, type: isSelect ? 'select' : 'text' })
|
||||||
|
setEditValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存编辑 */
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!editing || !editValue.trim()) { setEditing(null); return }
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const field = editing.field
|
||||||
|
if (FIELD_LABELS[field]) {
|
||||||
|
// 标准字段
|
||||||
|
await api.updateProfile({ [field]: editValue })
|
||||||
|
setProfile((prev) => prev ? { ...prev, [field]: editValue } : prev)
|
||||||
|
} else {
|
||||||
|
// key_facts
|
||||||
|
await api.updateProfile({ key_facts: { [field]: editValue } })
|
||||||
|
setProfile((prev) => prev ? { ...prev, key_facts: { ...prev.key_facts, [field]: editValue } } : prev)
|
||||||
|
}
|
||||||
|
setEditing(null)
|
||||||
|
} 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
|
||||||
@@ -537,15 +585,24 @@ export function MyProfile() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 标准字段:多列网格 */}
|
{/* 标准字段:多列网格(可编辑) */}
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:grid-cols-3">
|
||||||
{Object.entries(FIELD_LABELS).map(([key, label]) => {
|
{Object.entries(FIELD_LABELS).map(([key, label]) => {
|
||||||
const val = profile[key as keyof ProfileData] as string
|
const val = profile[key as keyof ProfileData] as string
|
||||||
if (!val) return null
|
if (!val) return null
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex items-center justify-between border-b pb-1">
|
<div key={key} className="group flex items-center justify-between border-b pb-1">
|
||||||
<span className="text-xs text-muted-foreground">{label}</span>
|
<span className="text-xs text-muted-foreground">{label}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-xs font-medium">{formatValue(key, val)}</span>
|
<span className="text-xs font-medium">{formatValue(key, val)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(key, label, val)}
|
||||||
|
className="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
aria-label={`编辑${label}`}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3 text-muted-foreground hover:text-primary" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -568,15 +625,24 @@ 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>
|
<p className="mb-1 text-xs text-muted-foreground">画像特征</p>
|
||||||
<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="flex items-center justify-between text-xs">
|
<div key={k} className="group flex items-center justify-between text-xs">
|
||||||
<span className="text-muted-foreground">{translateFactKey(k)}</span>
|
<span className="text-muted-foreground">{translateFactKey(k)}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<span className="font-medium">{translateFactValue(v)}</span>
|
<span className="font-medium">{translateFactValue(v)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(k, translateFactKey(k), v)}
|
||||||
|
className="opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
aria-label={`编辑${translateFactKey(k)}`}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3 text-muted-foreground hover:text-primary" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -677,6 +743,41 @@ export function MyProfile() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 编辑弹窗 */}
|
||||||
|
<Dialog open={!!editing} onOpenChange={(open) => !open && setEditing(null)}>
|
||||||
|
<DialogContent className="max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑{editing?.label}</DialogTitle>
|
||||||
|
<DialogDescription>修改后将保存为新的画像版本</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{editing?.type === 'select' && editing.field in FIELD_OPTIONS ? (
|
||||||
|
<Select value={editValue} onValueChange={setEditValue}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FIELD_OPTIONS[editing.field].map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
|
placeholder={`请输入${editing?.label}`}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setEditing(null)}>取消</Button>
|
||||||
|
<Button onClick={handleSave} disabled={saving || !editValue.trim()}>
|
||||||
|
{saving ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user