Files
WonderQ-Project/WonderQ-Admin-UI/src/components/admin/HomeContentCardRail.tsx
T
duanshuwen 7ba92f3c5d feat: 新增客片案例与团队共创模块,重构玩法与配置
- 新增客片案例全流程功能,包含前台页面、管理端配置、后端API、数据库迁移与文档
- 新增团队共创详情页面与对应接口
- 重构玩法模块:将静态playData替换为动态API获取,拆分类型定义到playTypes.ts
- 优化vite构建配置与环境变量处理,调整详情页操作栏文案
- 更新全量相关文档与配图,新增客片案例、团队共创API文档
- 新增测试用例,完善数据归一化逻辑
- 调整路由配置与环境变量示例文件
2026-08-19 20:23:04 +08:00

125 lines
5.2 KiB
TypeScript

import { ArrowDown, ArrowUp, Pencil, Plus, Trash2 } from "lucide-react";
import type { HomeExperience, HomeTeamBuilding, HomeWildArchive } from "@/api";
import { Button } from "@/components/ui/button";
export type HomeContentSection = "experiences" | "teamBuildings" | "wildArchives";
export type HomeContentItem = HomeExperience | HomeTeamBuilding | HomeWildArchive;
const sectionMeta: Record<HomeContentSection, { label: string; description: string; addLabel: string }> = {
experiences: { label: "体验推荐", description: "维护首页体验卡片和需求入口。", addLabel: "新增体验" },
teamBuildings: { label: "团队共创", description: "维护首页团建卡片和需求入口。", addLabel: "新增团建" },
wildArchives: { label: "极境视界", description: "维护首页案例卡片和需求入口。", addLabel: "新增案例" },
};
export function homeContentSectionMeta(section: HomeContentSection) {
return sectionMeta[section];
}
export function HomeContentSectionRail({
section,
items,
onSelect,
}: {
section: HomeContentSection;
items: Record<HomeContentSection, HomeContentItem[]>;
onSelect: (section: HomeContentSection) => void;
}) {
return (
<section className="module-rail">
<h3>首页内容</h3>
{(Object.keys(sectionMeta) as HomeContentSection[]).map((itemSection, index) => {
const meta = sectionMeta[itemSection];
const active = itemSection === section;
return (
<button
type="button"
className={`module-rail-item flex items-center gap-3 text-left${active ? " active" : ""}`}
key={itemSection}
aria-pressed={active}
onClick={() => onSelect(itemSection)}
>
<em>{index + 1}</em>
<span className="min-w-0">
<b className="block">{meta.label}</b>
<small className="block truncate">{items[itemSection].length} 条内容</small>
</span>
</button>
);
})}
</section>
);
}
export function HomeContentCardRail({
section,
items,
onCreate,
onEdit,
onDelete,
onMove,
moving,
}: {
section: HomeContentSection;
items: HomeContentItem[];
onCreate: () => void;
onEdit: (item: HomeContentItem) => void;
onDelete: (item: HomeContentItem) => void;
onMove: (itemId: string, direction: -1 | 1) => void;
moving?: boolean;
}) {
const meta = sectionMeta[section];
return (
<section className="item-rail">
<header className="module-data-head">
<span>
<h3>{meta.label}</h3>
<p>{meta.description}</p>
</span>
<Button type="button" className="primary-action compact" onClick={onCreate}>
<Plus size={16} />
{meta.addLabel}
</Button>
</header>
<div className="mapped-list">
{items.map((item, index) => (
<article className="mapped-list-row" key={item.id}>
<div className="mapped-list-main min-w-0">
{item.image ? <img src={item.image} alt="" /> : <span className="image-placeholder" />}
<span className="min-w-0">
<b className="block truncate">{item.title}</b>
<small className="block truncate">
{"badge" in item ? `${item.category} · ${item.englishTitle}` : "tag" in item ? `${item.tag} · ${item.detailParagraphs?.length ?? 0} 段正文` : `${item.images?.length ?? 0} 张照片 · ${item.demandKeyword}`}
</small>
</span>
</div>
<div className="flex items-center gap-1">
<span className={`hidden rounded-full px-2.5 py-1 text-xs font-semibold sm:inline-flex ${item.isActive ? "bg-emerald-50 text-emerald-700" : "bg-slate-100 text-slate-500"}`}>
{item.isActive ? "已启用" : "已停用"}
</span>
<Button type="button" variant="ghost" size="icon" className="module-rail-action" aria-label={`上移${item.title}`} disabled={index === 0 || moving} onClick={() => onMove(item.id, -1)}>
<ArrowUp size={15} />
</Button>
<Button type="button" variant="ghost" size="icon" className="module-rail-action" aria-label={`下移${item.title}`} disabled={index === items.length - 1 || moving} onClick={() => onMove(item.id, 1)}>
<ArrowDown size={15} />
</Button>
<Button type="button" variant="ghost" size="icon" className="module-rail-action" aria-label={`编辑${item.title}`} onClick={() => onEdit(item)}>
<Pencil size={15} />
</Button>
<Button type="button" variant="ghost" size="icon" className="module-rail-action danger" aria-label={`删除${item.title}`} onClick={() => onDelete(item)}>
<Trash2 size={15} />
</Button>
</div>
</article>
))}
{!items.length ? (
<div className="rounded-xl border border-dashed border-slate-300 bg-slate-50 p-8 text-center">
<p className="text-sm font-medium text-slate-700">暂无{meta.label}内容</p>
<p className="mt-1 text-xs leading-5 text-slate-500">点击右上角开始新增首页卡片。</p>
</div>
) : null}
</div>
</section>
);
}