feat(admin): add route sections admin management module

This commit adds the complete admin workflow for managing homepage curated route subgroups:
- Add `RouteSection` type and extend `SiteConfig`/`SiteModule` to support the new module
- Create all required admin components: panel, editors, product picker, and utility functions
- Update admin utilities and API types to handle routeSections CRUD operations
- Update documentation to include the new routeSections API contract
- Add supporting CSS styles for the new UI elements
This commit is contained in:
duanshuwen
2026-07-02 23:05:45 +08:00
parent 50be3cbfce
commit 9fd91c84e5
14 changed files with 1732 additions and 21 deletions

View File

@@ -25,6 +25,7 @@ import {
import type { Product, SiteConfig, SiteItemPatch, SiteModule } from "@/api";
import { AdminDisclosure } from "@/components/admin/AdminDisclosure";
import { EmptyState } from "@/components/admin/EmptyState";
import { RouteSectionsPanel } from "@/components/admin/route-sections/RouteSectionsPanel";
import { SingleImageUploader } from "@/components/admin/SingleImageUploader";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -102,9 +103,9 @@ const pageSpecs: {
frontPosition: "首页「特价优惠」",
},
{
id: "routeProducts",
id: "routeSections",
label: "精选线路",
hint: "从商品库选择上架线路,决定首页线路池。",
hint: "按运营任务新增精选线路分组,维护标题、副文案、启用状态和关联线路顺序。",
frontPosition: "首页精选线路",
},
{
@@ -217,20 +218,25 @@ export function StructurePage({
const [message, setMessage] = useState("");
const [editorOpen, setEditorOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [routeSectionCreateRequest, setRouteSectionCreateRequest] = useState(0);
const activePage =
pageSpecs.find((page) => page.id === (fixedPage ?? pageId)) ?? pageSpecs[0];
const activeModule =
activePage.modules.find((module) => module.id === moduleId) ??
activePage.modules[0];
const isRouteSectionsModule = moduleId === "routeSections";
const editableItems =
config && moduleEditable(moduleId) ? moduleItems(config, moduleId) : [];
config && moduleEditable(moduleId) && !isRouteSectionsModule
? moduleItems(config, moduleId)
: [];
const isMapModule = moduleId === "map";
const isCampaignModule = moduleId === "campaigns";
const canReorderSiteItems =
moduleEditable(moduleId) && !isMapModule && !isCampaignModule;
moduleEditable(moduleId) && !isRouteSectionsModule && !isMapModule && !isCampaignModule;
const canCreateSiteItem =
moduleEditable(moduleId) && (!isMapModule || editableItems.length === 0);
moduleEditable(moduleId) && !isRouteSectionsModule && (!isMapModule || editableItems.length === 0);
const showRouteSectionCreateButton = isRouteSectionsModule;
const isCreatingSiteItem = selectedId === NEW_SITE_ITEM_ID;
const selectedItem = isCreatingSiteItem
? undefined
@@ -239,6 +245,7 @@ export function StructurePage({
const showEditorDrawer =
editorOpen &&
moduleEditable(moduleId) &&
!isRouteSectionsModule &&
(isCreatingSiteItem || Boolean(selectedItem));
const gridClassName = [
"maintenance-grid",
@@ -271,7 +278,7 @@ export function StructurePage({
});
};
const load = async () => {
const load = async (): Promise<SiteConfig> => {
const [site, productResult] = await Promise.all([
getSiteConfig(),
getProducts(),
@@ -291,6 +298,7 @@ export function StructurePage({
setDraftFromItem(first);
}
}
return site;
};
useEffect(() => {
@@ -309,7 +317,7 @@ export function StructurePage({
}, [fixedPage]);
useEffect(() => {
if (!config || !moduleEditable(moduleId)) {
if (!config || !moduleEditable(moduleId) || isRouteSectionsModule) {
setSelectedId("");
setDraft({});
setEditorOpen(false);
@@ -338,7 +346,7 @@ export function StructurePage({
};
const startCreateSiteItem = () => {
if (!moduleEditable(moduleId)) return;
if (!moduleEditable(moduleId) || isRouteSectionsModule) return;
setSelectedId(NEW_SITE_ITEM_ID);
setDraft(createEmptySiteItemDraft(moduleId, editableItems.length));
setMessage("");
@@ -375,6 +383,7 @@ export function StructurePage({
saving ||
!config ||
!moduleEditable(moduleId) ||
isRouteSectionsModule ||
(!selectedId && !isCreatingSiteItem)
)
return;
@@ -451,7 +460,7 @@ export function StructurePage({
};
const removeSiteItem = async (item: EditableSiteItem) => {
if (!moduleEditable(moduleId)) return;
if (!moduleEditable(moduleId) || isRouteSectionsModule) return;
const confirmed = window.confirm(
`确认删除「${itemName(item)}」吗?删除后需要后端同步移除对应模块配置。`,
);
@@ -477,7 +486,7 @@ export function StructurePage({
};
const moveSiteItem = async (item: EditableSiteItem, direction: -1 | 1) => {
if (!moduleEditable(moduleId)) return;
if (!moduleEditable(moduleId) || isRouteSectionsModule) return;
const currentIndex = editableItems.findIndex(
(candidate) => candidate.id === item.id,
);
@@ -621,9 +630,27 @@ export function StructurePage({
<Plus size={16} />
{isMapModule ? "上传地图" : "新增"}
</Button>
) : showRouteSectionCreateButton ? (
<Button
className="primary-action compact"
onClick={() => setRouteSectionCreateRequest((current) => current + 1)}
>
<Plus size={16} />
</Button>
) : null}
</header>
{moduleEditable(moduleId) ? (
{isRouteSectionsModule ? (
<RouteSectionsPanel
routeSections={config.routeSections}
products={products}
moduleLabel={activeModule.label}
onDirtyChange={onDirtyChange}
notify={notify}
onReload={load}
createRequestKey={routeSectionCreateRequest}
/>
) : moduleEditable(moduleId) ? (
<div className="mapped-list">
{editableItems.map((item, index) => (
<div className="mapped-list-row" key={item.id}>