refactor(frontend): consolidate features & components into single tree
把 components/admin/* 和 components/platform/* 共 11 个文件
移到对应 features/<name>/ 下,components/ 只保留跨特性共享的
common/ 子目录(features/admin/* 之前和 components/admin/* 同名
共存,迫使 features/admin/AdminPages.tsx 出现 barrel,顺带消除了
barrel 副作用 CSS 丢失的隐患)。
新的目录约定:
- features/<name>/ = 特性模块,所有页面 / Modal / state / hooks
/ routes 都在内,无 components/<name>/ 并列
- components/common/ = 仅放 ≥2 个特性共用的 widgets
(Icon、Sidebar、Toast、Topbar、WelcomePanel)
改动:
- 11 文件 git mv (components/{admin,platform}/* → features/*)
- 8 文件的 Icon 相对路径 '../common/Icon' → '../../components/common/Icon'
- 2 文件补 admin.css side-effect import(UserManagementPage /
ProjectManagementPage 之前依赖 AdminPages barrel)
- 3 文件(引用方)更新 import:SystemAdminPage、DashboardRoute、
ScriptsPage
- 删除 features/admin/AdminPages.tsx barrel(无消费者)
- 删除空目录 components/admin、components/platform
验证:
- pnpm typecheck 通过
- pnpm build 通过,CSS 体积 35.11 kB 与重构前一致(无样式增减)
- routes.ts / routes/platform.tsx 路径未动,route id 不变
踩坑:
第一轮把 '../common/Icon' 错改成 '../../../components/common/Icon',
typecheck 报 11 个 Cannot find module。原因:features/admin/ 和
components/admin/ 都是 depth 2,'../../X' 在两边都解析到 app/X,
只有 '../X' 才需要多一层。正确改法是 '../../components/common/Icon'。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
80c92f7fd4
commit
91031342b8
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { type Employee } from "../../services/api";
|
||||
|
||||
// 多选用户下拉框组件 - 带 tag 显示
|
||||
export function UserMultiSelect({
|
||||
users,
|
||||
selectedUserIds,
|
||||
onChange,
|
||||
existingMemberIds,
|
||||
}: {
|
||||
users: Employee[];
|
||||
selectedUserIds: string[];
|
||||
onChange: (ids: string[]) => void;
|
||||
existingMemberIds?: string[];
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 点击外部关闭下拉框
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const selectedUsers = users.filter((user) => selectedUserIds.includes(user.user_id));
|
||||
|
||||
const toggleUser = (userId: string) => {
|
||||
if (selectedUserIds.includes(userId)) {
|
||||
onChange(selectedUserIds.filter((id) => id !== userId));
|
||||
} else {
|
||||
onChange([...selectedUserIds, userId]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeUser = (userId: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onChange(selectedUserIds.filter((id) => id !== userId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={{ position: "relative" }}>
|
||||
{/* 选择框 - 显示 tag */}
|
||||
<div
|
||||
className="admin-toolbar__input"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: "4px",
|
||||
padding: "6px 32px 6px 8px",
|
||||
minHeight: "36px",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "#fff",
|
||||
}}
|
||||
>
|
||||
{selectedUsers.length === 0 ? (
|
||||
<span style={{ color: "#999" }}>请选择用户</span>
|
||||
) : (
|
||||
selectedUsers.map((user) => (
|
||||
<span
|
||||
key={user.user_id}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#e6f7ff",
|
||||
color: "#1890ff",
|
||||
border: "1px solid #91d5ff",
|
||||
borderRadius: "2px",
|
||||
padding: "2px 6px",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
{user.display_name}
|
||||
<span
|
||||
onClick={(e) => removeUser(user.user_id, e)}
|
||||
style={{
|
||||
marginLeft: "4px",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "12px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
color: "#999",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{isOpen ? "▲" : "▼"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 下拉列表 */}
|
||||
{isOpen && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: "136px",
|
||||
overflowY: "auto",
|
||||
border: "1px solid #1890ff",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#fff",
|
||||
zIndex: 100,
|
||||
marginTop: "2px",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||
}}
|
||||
>
|
||||
{users.length === 0 ? (
|
||||
<div style={{ padding: "12px", color: "#999", textAlign: "center", fontSize: "13px" }}>
|
||||
所有可选用户已在项目中
|
||||
</div>
|
||||
) : (
|
||||
users.map((user) => {
|
||||
const isSelected = selectedUserIds.includes(user.user_id);
|
||||
const isExisting = existingMemberIds?.includes(user.user_id);
|
||||
const isDisabled = isExisting;
|
||||
return (
|
||||
<div
|
||||
key={user.user_id}
|
||||
onClick={() => !isDisabled && toggleUser(user.user_id)}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "6px 12px",
|
||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||
backgroundColor: isSelected ? "#e6f7ff" : isExisting ? "#f5f5f5" : "transparent",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
opacity: isDisabled ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{user.display_name} <span style={{ color: "#999" }}>({user.username})</span>
|
||||
</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
{isExisting && (
|
||||
<span style={{ fontSize: "11px", color: "#999", backgroundColor: "#f0f0f0", padding: "1px 6px", borderRadius: "2px" }}>
|
||||
已在项目中
|
||||
</span>
|
||||
)}
|
||||
{isSelected && <span style={{ color: "#1890ff" }}>✓</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user