- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
165 lines
6.4 KiB
TypeScript
165 lines
6.4 KiB
TypeScript
'use client';
|
||
import { useState } from 'react';
|
||
import { X, Plus, Pencil, Trash2, Check } from 'lucide-react';
|
||
import type { DictItem, SourceTarget, SourceType } from '@/lib/requirement';
|
||
import { SOURCE_TYPE_LABEL } from '@/lib/requirement';
|
||
|
||
interface DictDrawerProps {
|
||
open: boolean;
|
||
title: string;
|
||
items: DictItem[];
|
||
onClose: () => void;
|
||
onAdd: (name: string) => void;
|
||
onUpdate: (id: string, name: string) => void;
|
||
onDelete: (id: string) => void;
|
||
}
|
||
|
||
interface SourceDrawerProps {
|
||
open: boolean;
|
||
items: SourceTarget[];
|
||
onClose: () => void;
|
||
onAdd: (name: string, sourceType: SourceType) => void;
|
||
onUpdate: (id: string, name: string) => void;
|
||
onDelete: (id: string) => void;
|
||
}
|
||
|
||
const SOURCE_TYPES: SourceType[] = ['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management'];
|
||
|
||
export function DictDrawer({ open, title, items, onClose, onAdd, onUpdate, onDelete }: DictDrawerProps) {
|
||
if (!open) return null;
|
||
return (
|
||
<DrawerShell title={title} onClose={onClose}>
|
||
<ItemList items={items} onAdd={onAdd} onUpdate={onUpdate} onDelete={onDelete} />
|
||
</DrawerShell>
|
||
);
|
||
}
|
||
|
||
export function SourceDrawer({ open, items, onClose, onAdd, onUpdate, onDelete }: SourceDrawerProps) {
|
||
const [activeTab, setActiveTab] = useState<SourceType>('customer');
|
||
|
||
if (!open) return null;
|
||
|
||
const filtered = items.filter((t) => t.sourceType === activeTab);
|
||
|
||
return (
|
||
<DrawerShell title="来源对象管理" onClose={onClose}>
|
||
{/* Tabs */}
|
||
<div className="flex flex-wrap gap-1 px-4 py-2 border-b border-[var(--line)]">
|
||
{SOURCE_TYPES.map((type) => (
|
||
<button
|
||
key={type}
|
||
onClick={() => setActiveTab(type)}
|
||
className={`h-7 rounded-md px-2.5 text-[11px] font-medium transition-colors ${activeTab === type ? 'bg-[var(--accent)] text-white' : 'border border-[var(--line)] text-[var(--ink-soft)] hover:text-[var(--ink)]'}`}
|
||
>
|
||
{SOURCE_TYPE_LABEL[type]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<ItemList
|
||
items={filtered}
|
||
onAdd={(name) => onAdd(name, activeTab)}
|
||
onUpdate={onUpdate}
|
||
onDelete={onDelete}
|
||
placeholder={`添加${SOURCE_TYPE_LABEL[activeTab]}来源对象`}
|
||
/>
|
||
</DrawerShell>
|
||
);
|
||
}
|
||
|
||
function DrawerShell({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) {
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex justify-end bg-black/30" onClick={onClose}>
|
||
<div className="w-80 h-full bg-[var(--bg-card)] border-l border-[var(--line)] flex flex-col" onClick={(e) => e.stopPropagation()}>
|
||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--line)]">
|
||
<span className="text-[13px] font-semibold text-[var(--ink)]">{title}</span>
|
||
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)]">
|
||
<X className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
{children}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ItemList({ items, onAdd, onUpdate, onDelete, placeholder }: {
|
||
items: { id: string; name: string }[];
|
||
onAdd: (name: string) => void;
|
||
onUpdate: (id: string, name: string) => void;
|
||
onDelete: (id: string) => void;
|
||
placeholder?: string;
|
||
}) {
|
||
const [newName, setNewName] = useState('');
|
||
const [editingId, setEditingId] = useState<string | null>(null);
|
||
const [editingName, setEditingName] = useState('');
|
||
|
||
const handleAdd = () => {
|
||
const trimmed = newName.trim();
|
||
if (!trimmed) return;
|
||
onAdd(trimmed);
|
||
setNewName('');
|
||
};
|
||
|
||
const confirmEdit = () => {
|
||
if (editingId && editingName.trim()) {
|
||
onUpdate(editingId, editingName.trim());
|
||
}
|
||
setEditingId(null);
|
||
setEditingName('');
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div className="flex items-center gap-2 px-4 py-3 border-b border-[var(--line)]">
|
||
<input
|
||
type="text"
|
||
value={newName}
|
||
onChange={(e) => setNewName(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
||
placeholder={placeholder || '输入名称...'}
|
||
className="flex-1 h-8 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] outline-none focus:border-[var(--accent)]"
|
||
/>
|
||
<button onClick={handleAdd} className="h-8 w-8 flex items-center justify-center rounded-lg bg-[var(--accent)] text-white hover:opacity-90">
|
||
<Plus className="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto">
|
||
{items.map((item) => (
|
||
<div key={item.id} className="group flex items-center px-4 py-2 hover:bg-[var(--bg-subtle)]">
|
||
{editingId === item.id ? (
|
||
<div className="flex items-center gap-2 flex-1">
|
||
<input
|
||
type="text"
|
||
value={editingName}
|
||
onChange={(e) => setEditingName(e.target.value)}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') confirmEdit(); if (e.key === 'Escape') setEditingId(null); }}
|
||
autoFocus
|
||
className="flex-1 h-7 rounded-lg border border-[var(--accent)] bg-[var(--bg-card)] px-2 text-[13px] text-[var(--ink)] outline-none"
|
||
/>
|
||
<button onClick={confirmEdit} className="p-1 rounded hover:bg-[var(--bg-subtle)] text-[var(--accent)]">
|
||
<Check className="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<span className="flex-1 text-[13px] text-[var(--ink)]">{item.name}</span>
|
||
<div className="hidden group-hover:flex items-center gap-1">
|
||
<button onClick={() => { setEditingId(item.id); setEditingName(item.name); }} className="p-1 rounded hover:bg-[var(--bg-card)] text-[var(--ink-muted)]">
|
||
<Pencil className="h-3.5 w-3.5" />
|
||
</button>
|
||
<button onClick={() => onDelete(item.id)} className="p-1 rounded hover:bg-[var(--bg-card)] text-[var(--ink-muted)] hover:text-red-500">
|
||
<Trash2 className="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
))}
|
||
{items.length === 0 && (
|
||
<div className="px-4 py-8 text-center text-[12px] text-[var(--ink-muted)]">暂无数据,请添加</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
);
|
||
}
|