Files
Script Generator 9a0b16a8f1 feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择
- 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出
- 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置
- 角色管理:卡片列表、系统角色保护、CRUD
- 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页
- 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 18:16:18 +08:00

165 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
</>
);
}