| 1 | /** |
| 2 | * EditTitleModal - 编辑标题弹窗组件 |
| 3 | * 功能:编辑任务标题,支持字数限制和回车提交 |
| 4 | */ |
| 5 | |
| 6 | 'use client' |
| 7 | |
| 8 | import { memo, useCallback, useEffect, useState } from 'react' |
| 9 | import { useTransClient } from '@/app/i18n/client' |
| 10 | import { Input } from '@/components/ui/input' |
| 11 | import { Label } from '@/components/ui/label' |
| 12 | import { Modal } from '@/components/ui/modal' |
| 13 | |
| 14 | export interface IEditTitleModalProps { |
| 15 | /** 是否显示 */ |
| 16 | open: boolean |
| 17 | /** 关闭回调 */ |
| 18 | onOpenChange: (open: boolean) => void |
| 19 | /** 当前标题 */ |
| 20 | currentTitle: string |
| 21 | /** 保存回调 */ |
| 22 | onSave: (title: string) => Promise<void> |
| 23 | /** 最大字数限制 */ |
| 24 | maxLength?: number |
| 25 | } |
| 26 | |
| 27 | /** 内部内容组件 */ |
| 28 | const ModalContent = memo( |
| 29 | ({ onOpenChange, currentTitle, onSave, maxLength = 100 }: Omit<IEditTitleModalProps, 'open'>) => { |
| 30 | const { t } = useTransClient('chat') |
| 31 | const [title, setTitle] = useState(currentTitle) |
| 32 | const [isLoading, setIsLoading] = useState(false) |
| 33 | |
| 34 | // 当 currentTitle 变化时更新内部状态 |
| 35 | useEffect(() => { |
| 36 | setTitle(currentTitle) |
| 37 | }, [currentTitle]) |
| 38 | |
| 39 | const handleSave = useCallback(async () => { |
| 40 | const trimmedTitle = title.trim() |
| 41 | if (!trimmedTitle || isLoading) |
| 42 | return |
| 43 | |
| 44 | setIsLoading(true) |
| 45 | try { |
| 46 | await onSave(trimmedTitle) |
| 47 | onOpenChange(false) |
| 48 | } |
| 49 | finally { |
| 50 | setIsLoading(false) |
| 51 | } |
| 52 | }, [title, isLoading, onSave, onOpenChange]) |
| 53 | |
| 54 | const handleKeyDown = useCallback( |
| 55 | (e: React.KeyboardEvent) => { |
| 56 | if (e.key === 'Enter' && !e.shiftKey) { |
| 57 | e.preventDefault() |
| 58 | handleSave() |
| 59 | } |
| 60 | }, |
| 61 | [handleSave], |
| 62 | ) |
| 63 | |
| 64 | const handleClose = useCallback(() => { |
| 65 | onOpenChange(false) |
| 66 | }, [onOpenChange]) |
| 67 | |
| 68 | return ( |
| 69 | <Modal |
| 70 | open |
| 71 | title={t('task.editTitle')} |
| 72 | onCancel={handleClose} |
| 73 | onOk={handleSave} |
| 74 | confirmLoading={isLoading} |
| 75 | okText={t('rating.submit')} |
| 76 | cancelText={t('rating.cancel')} |
| 77 | width={400} |
| 78 | > |
| 79 | <div className="space-y-4 py-2"> |
| 80 | <div className="space-y-2"> |
| 81 | <Label htmlFor="task-title">{t('task.titleLabel')}</Label> |
| 82 | <Input |
| 83 | id="task-title" |
| 84 | value={title} |
| 85 | onChange={e => setTitle(e.target.value)} |
| 86 | onKeyDown={handleKeyDown} |
| 87 | placeholder={t('task.titlePlaceholder')} |
| 88 | maxLength={maxLength} |
| 89 | autoFocus |
| 90 | /> |
| 91 | <div className="text-xs text-muted-foreground text-right"> |
| 92 | {title.length} |
| 93 | {' '} |
| 94 | / |
| 95 | {maxLength} |
| 96 | </div> |
| 97 | </div> |
| 98 | </div> |
| 99 | </Modal> |
| 100 | ) |
| 101 | }, |
| 102 | ) |
| 103 | |
| 104 | /** |
| 105 | * EditTitleModal - 编辑标题弹窗 |
| 106 | * 使用两层组件模式避免动态加载 namespace 导致闪烁 |
| 107 | */ |
| 108 | export function EditTitleModal({ open, ...props }: IEditTitleModalProps) { |
| 109 | // 只在打开时渲染内部组件,避免动态加载 namespace 导致闪烁 |
| 110 | if (!open) |
| 111 | return null |
| 112 | |
| 113 | return <ModalContent {...props} /> |
| 114 | } |
| 115 | |
| 116 | export default EditTitleModal |
| 117 |