- 新增客片案例全流程功能,包含前台页面、管理端配置、后端API、数据库迁移与文档 - 新增团队共创详情页面与对应接口 - 重构玩法模块:将静态playData替换为动态API获取,拆分类型定义到playTypes.ts - 优化vite构建配置与环境变量处理,调整详情页操作栏文案 - 更新全量相关文档与配图,新增客片案例、团队共创API文档 - 新增测试用例,完善数据归一化逻辑 - 调整路由配置与环境变量示例文件
125 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|