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:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user