- 移除亲情卡支出分类(前端+后端) - 搜索结果添加点击编辑功能 - 提取 RecordModal 组件复用(index.js/search.js) - 添加 useMemo 优化计算性能 - 优化类别查找为 O(1) 映射 - 添加删除分类接口输入验证 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
4.7 KiB
JavaScript
147 lines
4.7 KiB
JavaScript
import { Icon } from './icons';
|
||
|
||
export default function RecordModal({
|
||
isOpen,
|
||
onClose,
|
||
onSubmit,
|
||
onDelete,
|
||
formData,
|
||
setFormData,
|
||
formError,
|
||
submitting,
|
||
deleting,
|
||
editingRecord,
|
||
categories,
|
||
}) {
|
||
if (!isOpen) return null;
|
||
|
||
const handleTypeChange = (type) => {
|
||
setFormData({ ...formData, type, category: '' });
|
||
};
|
||
|
||
const isDisabled = submitting || deleting;
|
||
|
||
return (
|
||
<div className="modal-overlay modal-overlay--fullscreen" onClick={onClose}>
|
||
<div className="modal modal--fullscreen" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<div>
|
||
<p className="eyebrow">{editingRecord ? '编辑记录' : '新增记录'}</p>
|
||
{editingRecord ? <h2>更新账目</h2> : null}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="icon-button icon-button--ghost"
|
||
aria-label="关闭弹窗"
|
||
onClick={onClose}
|
||
disabled={isDisabled}
|
||
>
|
||
<Icon name="close" size={18} />
|
||
</button>
|
||
</div>
|
||
{formError && <div className="error-msg">{formError}</div>}
|
||
<form onSubmit={onSubmit}>
|
||
<div className="type-switch">
|
||
<button
|
||
type="button"
|
||
className={`type-btn expense ${formData.type === 'expense' ? 'active' : ''}`}
|
||
onClick={() => handleTypeChange('expense')}
|
||
disabled={isDisabled}
|
||
>
|
||
支出
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`type-btn income ${formData.type === 'income' ? 'active' : ''}`}
|
||
onClick={() => handleTypeChange('income')}
|
||
disabled={isDisabled}
|
||
>
|
||
收入
|
||
</button>
|
||
</div>
|
||
<div className="form-group">
|
||
<label htmlFor="record-amount">金额</label>
|
||
<input
|
||
id="record-amount"
|
||
type="number"
|
||
className="amount-input"
|
||
value={formData.amount}
|
||
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
|
||
placeholder="0.00"
|
||
step="0.01"
|
||
required
|
||
disabled={isDisabled}
|
||
/>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>分类</label>
|
||
<div className="category-grid">
|
||
{categories[formData.type].map((cat) => (
|
||
<button
|
||
key={cat.name}
|
||
type="button"
|
||
className={`category-btn ${formData.category === cat.name ? 'active' : ''}`}
|
||
onClick={() => setFormData({ ...formData, category: cat.name })}
|
||
disabled={isDisabled}
|
||
>
|
||
<span className="icon">{cat.icon}</span>
|
||
<span>{cat.name}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>日期</label>
|
||
<div className="datetime-picker">
|
||
<input
|
||
type="date"
|
||
value={formData.date}
|
||
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
|
||
required
|
||
disabled={isDisabled}
|
||
/>
|
||
<input
|
||
type="time"
|
||
value={formData.time || '00:00'}
|
||
onChange={(e) => setFormData({ ...formData, time: e.target.value })}
|
||
disabled={isDisabled}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="form-group">
|
||
<label htmlFor="record-note">备注(可选)</label>
|
||
<input
|
||
id="record-note"
|
||
type="text"
|
||
value={formData.note}
|
||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||
placeholder="添加备注..."
|
||
disabled={isDisabled}
|
||
/>
|
||
</div>
|
||
<div className="modal-actions">
|
||
<button type="submit" className="btn btn-primary" disabled={isDisabled}>
|
||
{submitting ? (
|
||
<span className="button-content">
|
||
<Icon name="spinner" size={16} className="is-spinning" />
|
||
保存中...
|
||
</span>
|
||
) : '保存'}
|
||
</button>
|
||
{editingRecord && (
|
||
<button
|
||
type="button"
|
||
className="btn btn-danger"
|
||
onClick={onDelete}
|
||
disabled={isDisabled}
|
||
>
|
||
{deleting ? '删除中...' : '删除'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|