refactor(inbox): 侧边栏素材列表改为轻量 Flex 布局 — Card→Flex, 新增状态 Tag, hover 切换删除按钮, 左侧竖线选中态

This commit is contained in:
2026-06-03 16:21:56 +08:00
parent 21b557c255
commit abe30ead6a
3 changed files with 132 additions and 36 deletions

View File

@@ -1,7 +1,7 @@
import { DeleteOutlined } from "@ant-design/icons"; import { DeleteOutlined } from "@ant-design/icons";
import { Button, Card, Flex, Popconfirm, Typography } from "antd"; import { Button, Flex, Popconfirm, Tag, Typography } from "antd";
import type { Material } from "../types"; import type { Material, MaterialStatus } from "../types";
interface MaterialCardProps { interface MaterialCardProps {
material: Material; material: Material;
@@ -10,34 +10,55 @@ interface MaterialCardProps {
selected: boolean; selected: boolean;
} }
export function MaterialCard({ material, onDelete, onSelect }: MaterialCardProps) { const STATUS_MAP: Record<MaterialStatus, { color: string; label: string }> = {
approved: { color: "green", label: "已通过" },
discarded: { color: "red", label: "已放弃" },
pending: { color: "gold", label: "待审核" },
};
export function MaterialCard({ material, onDelete, onSelect, selected }: MaterialCardProps) {
const statusInfo = STATUS_MAP[material.status];
const className = selected ? "material-list-item material-list-item--selected" : "material-list-item";
return ( return (
<Card hoverable={false} onClick={onSelect} size="small"> <Flex align="center" className={className} gap="small" justify="space-between" onClick={onSelect}>
<Typography.Paragraph ellipsis={{ rows: 3 }}>{material.description}</Typography.Paragraph> <div style={{ flex: 1, minWidth: 0 }}>
<Flex align="center" justify="space-between"> <Typography.Text ellipsis strong={selected}>
<Typography.Text type="secondary">{formatMaterialTime(material.createdAt)}</Typography.Text> {material.description}
<Popconfirm </Typography.Text>
description="删除后不可恢复" <br />
okButtonProps={{ danger: true }} <Typography.Text className="material-item-time" type="secondary">
okText="删除" {formatMaterialTime(material.createdAt)}
onCancel={(e) => e?.stopPropagation()} </Typography.Text>
onConfirm={(e) => { </div>
e?.stopPropagation(); <div className="material-item-right">
onDelete(); <span className="material-item-tag">
}} {statusInfo && <Tag color={statusInfo.color}>{statusInfo.label}</Tag>}
title="确认删除该素材?" </span>
> <span className="material-item-actions">
<Button <Popconfirm
aria-label="删除" description="删除后不可恢复"
danger okButtonProps={{ danger: true }}
icon={<DeleteOutlined />} okText="删除"
onClick={(e) => e.stopPropagation()} onCancel={(e) => e?.stopPropagation()}
size="small" onConfirm={(e) => {
type="text" e?.stopPropagation();
/> onDelete();
</Popconfirm> }}
</Flex> title="确认删除该素材?"
</Card> >
<Button
aria-label="删除"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
size="small"
type="text"
/>
</Popconfirm>
</span>
</div>
</Flex>
); );
} }

View File

@@ -286,7 +286,6 @@ body {
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
gap: var(--ant-margin-xs);
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
@@ -304,3 +303,65 @@ body {
.app-inbox-datepicker { .app-inbox-datepicker {
width: 100%; width: 100%;
} }
/* Inbox material list items */
.material-list-item {
border-left: 3px solid transparent;
border-bottom: 1px solid var(--ant-color-border-secondary);
padding: var(--ant-padding-xs) 0;
padding-left: var(--ant-padding-sm);
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.material-list-item:last-child {
border-bottom: none;
}
.material-list-item:hover {
background: var(--ant-color-fill-tertiary);
}
.material-list-item--selected {
border-left-color: var(--ant-color-primary);
}
.material-list-item--selected:hover {
background: var(--ant-color-fill-tertiary);
}
.material-item-right {
position: relative;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.material-item-tag,
.material-item-actions {
transition: opacity 0.15s ease;
}
.material-item-tag {
opacity: 1;
}
.material-item-actions {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
opacity: 0;
}
.material-list-item:hover .material-item-tag {
opacity: 0;
}
.material-list-item:hover .material-item-actions {
opacity: 1;
}
.material-item-time {
font-size: var(--ant-font-size-sm);
}

View File

@@ -18,7 +18,7 @@ const MOCK_MATERIAL: Material = {
}; };
describe("MaterialCard", () => { describe("MaterialCard", () => {
test("渲染素材描述和创建时间", () => { test("渲染素材描述、时间和状态标签", () => {
renderWithProviders( renderWithProviders(
createElement(MaterialCard, { createElement(MaterialCard, {
material: MOCK_MATERIAL, material: MOCK_MATERIAL,
@@ -29,6 +29,7 @@ describe("MaterialCard", () => {
); );
expect(screen.getByText("测试素材描述")).not.toBeNull(); expect(screen.getByText("测试素材描述")).not.toBeNull();
expect(screen.getByText("今天")).not.toBeNull(); expect(screen.getByText("今天")).not.toBeNull();
expect(screen.getByText("待审核")).not.toBeNull();
}); });
test("点击卡片触发 onSelect", () => { test("点击卡片触发 onSelect", () => {
@@ -41,8 +42,8 @@ describe("MaterialCard", () => {
selected: false, selected: false,
}), }),
); );
const card = screen.getByText("测试素材描述").closest(".ant-card")!; const item = screen.getByText("测试素材描述").closest(".material-list-item")!;
fireEvent.click(card); fireEvent.click(item);
expect(onSelect).toHaveBeenCalledTimes(1); expect(onSelect).toHaveBeenCalledTimes(1);
}); });
@@ -71,7 +72,7 @@ describe("MaterialCard", () => {
expect(onSelect).not.toHaveBeenCalled(); expect(onSelect).not.toHaveBeenCalled();
}); });
test("选中状态不再使用 app-inbox-card-selected 类", () => { test("选中时包含 material-list-item--selected 类", () => {
renderWithProviders( renderWithProviders(
createElement(MaterialCard, { createElement(MaterialCard, {
material: MOCK_MATERIAL, material: MOCK_MATERIAL,
@@ -80,7 +81,20 @@ describe("MaterialCard", () => {
selected: true, selected: true,
}), }),
); );
const card = screen.getByText("测试素材描述").closest(".app-inbox-card-selected"); const item = screen.getByText("测试素材描述").closest(".material-list-item--selected");
expect(card).toBeNull(); expect(item).not.toBeNull();
});
test("未选中时不包含 material-list-item--selected 类名", () => {
renderWithProviders(
createElement(MaterialCard, {
material: MOCK_MATERIAL,
onDelete: vi.fn(),
onSelect: vi.fn(),
selected: false,
}),
);
const item = screen.getByText("测试素材描述").closest(".material-list-item--selected");
expect(item).toBeNull();
}); });
}); });