From 548f91c37f384c5560c0c69cef5365d0844d81a4 Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Mon, 17 Aug 2026 22:43:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B8=85=E7=90=86=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E4=B8=9A=E5=8A=A1=E6=A8=A1=E5=9D=97=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=85=A8=E6=A0=88=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除后端产品、目的地、活动专题等废弃模块的数据库表与业务代码,删除冗余API接口 - 删除小程序端详情页、线路组件等冗余代码,移除搜索工具与测试用例,调整导航逻辑 - 清理管理端废弃的类型定义、编辑器与测试代码 - 更新项目文档,修正模块维护说明与接口文档内容 --- AGENTS.md | 26 +- README.md | 2 +- WonderQ-Admin-UI/src/api.ts | 254 +-- .../src/components/admin/AdminShell.tsx | 115 +- .../RouteProductQuickCreateForm.tsx | 251 --- .../route-sections/RouteSectionEditor.tsx | 107 -- .../RouteSectionProductEditor.tsx | 70 - .../RouteSectionProductPicker.tsx | 160 -- .../route-sections/RouteSectionsPanel.tsx | 621 -------- .../admin/route-sections/helpers.ts | 63 - WonderQ-Admin-UI/src/lib/admin-utils.ts | 319 +--- .../src/pages/leads/LeadsPage.tsx | 236 --- .../src/pages/leads/lead-helpers.ts | 91 -- .../src/pages/login/LoginPage.tsx | 2 +- .../src/pages/products/ProductsPage.tsx | 487 ------ .../src/pages/structure/StructurePage.tsx | 836 ++-------- WonderQ-Admin-UI/src/styles.css | 1415 +---------------- WonderQ-Admin-UI/src/types/admin.ts | 18 +- WonderQ-Admin/README.md | 15 +- .../alembic/versions/0002_add_map_image.py | 14 +- .../alembic/versions/0004_route_sections.py | 12 - .../versions/0008_demand_page_modules.py | 11 - .../versions/0009_remove_product_domain.py | 57 + .../0010_remove_destination_module.py | 45 + .../versions/0011_remove_cta_banner_module.py | 26 + .../versions/0012_remove_offer_modules.py | 27 + .../versions/0013_remove_map_theme_modules.py | 27 + .../0014_remove_destination_regions_module.py | 24 + WonderQ-Admin/app/content.py | 95 -- WonderQ-Admin/app/demand_recommendations.py | 66 - WonderQ-Admin/app/models.py | 226 +-- WonderQ-Admin/app/route_sections.py | 91 -- WonderQ-Admin/app/routers/admin.py | 418 +---- WonderQ-Admin/app/routers/public.py | 73 +- WonderQ-Admin/app/routers/shared.py | 41 +- WonderQ-Admin/app/schemas.py | 85 - WonderQ-Admin/app/seed.py | 217 +-- WonderQ-Admin/app/serializers.py | 72 +- WonderQ-Admin/data/generated-products.json | 290 ---- WonderQ-Admin/tests/test_api_contracts.py | 1160 +------------- WonderQ-Admin/tests/test_schemas.py | 9 +- WonderQ-MiniAPP/scripts/probe-secondary.mjs | 31 - WonderQ-MiniAPP/src/App.vue | 3 +- WonderQ-MiniAPP/src/app.css | 13 - .../src/components/MineProductRow.vue | 41 - WonderQ-MiniAPP/src/components/TopHeader.vue | 12 +- WonderQ-MiniAPP/src/content.ts | 130 -- WonderQ-MiniAPP/src/generated-products.json | 290 ---- WonderQ-MiniAPP/src/lib/api.ts | 8 +- WonderQ-MiniAPP/src/lib/data.ts | 623 +------- WonderQ-MiniAPP/src/lib/navigation.ts | 22 +- WonderQ-MiniAPP/src/lib/searchFilters.ts | 86 - WonderQ-MiniAPP/src/lib/store.ts | 97 +- WonderQ-MiniAPP/src/lib/types.ts | 362 +---- WonderQ-MiniAPP/src/pages.json | 6 - .../detail/components/DetailActionBar.vue | 56 - .../pages/detail/components/DetailHero.vue | 30 - .../detail/components/DetailInfoCard.vue | 56 - .../detail/components/DetailMediaGallery.vue | 16 - .../detail/components/DetailOverviewCard.vue | 49 - .../detail/components/detailPresentation.ts | 78 - WonderQ-MiniAPP/src/pages/detail/index.vue | 111 -- .../home/components/homeExperienceData.ts | 6 +- .../home/components/homeTeamBuildingData.ts | 8 +- .../home/components/homeWildArchivesData.ts | 10 +- WonderQ-MiniAPP/src/pages/home/index.vue | 12 +- .../src/pages/mine/components/MineGuest.vue | 7 +- .../src/pages/mine/components/MineMember.vue | 98 +- WonderQ-MiniAPP/src/pages/mine/index.vue | 22 +- .../src/pages/play/components/playData.ts | 6 +- WonderQ-MiniAPP/src/pages/play/index.vue | 4 +- WonderQ-MiniAPP/tests/data.test.ts | 397 +---- WonderQ-MiniAPP/tests/searchFilters.test.ts | 102 -- docs/admin-api-requirements.md | 794 +-------- docs/backend-plan.md | 4 +- docs/decisions.md | 2 +- docs/development-status.md | 104 +- docs/integration-workflow.md | 9 +- docs/module-config-api.md | 1051 +----------- docs/public-api.md | 725 ++------- 80 files changed, 953 insertions(+), 12702 deletions(-) delete mode 100644 WonderQ-Admin-UI/src/components/admin/route-sections/RouteProductQuickCreateForm.tsx delete mode 100644 WonderQ-Admin-UI/src/components/admin/route-sections/RouteSectionEditor.tsx delete mode 100644 WonderQ-Admin-UI/src/components/admin/route-sections/RouteSectionProductEditor.tsx delete mode 100644 WonderQ-Admin-UI/src/components/admin/route-sections/RouteSectionProductPicker.tsx delete mode 100644 WonderQ-Admin-UI/src/components/admin/route-sections/RouteSectionsPanel.tsx delete mode 100644 WonderQ-Admin-UI/src/components/admin/route-sections/helpers.ts delete mode 100644 WonderQ-Admin-UI/src/pages/leads/LeadsPage.tsx delete mode 100644 WonderQ-Admin-UI/src/pages/leads/lead-helpers.ts delete mode 100644 WonderQ-Admin-UI/src/pages/products/ProductsPage.tsx create mode 100644 WonderQ-Admin/alembic/versions/0009_remove_product_domain.py create mode 100644 WonderQ-Admin/alembic/versions/0010_remove_destination_module.py create mode 100644 WonderQ-Admin/alembic/versions/0011_remove_cta_banner_module.py create mode 100644 WonderQ-Admin/alembic/versions/0012_remove_offer_modules.py create mode 100644 WonderQ-Admin/alembic/versions/0013_remove_map_theme_modules.py create mode 100644 WonderQ-Admin/alembic/versions/0014_remove_destination_regions_module.py delete mode 100644 WonderQ-Admin/app/demand_recommendations.py delete mode 100644 WonderQ-Admin/app/route_sections.py delete mode 100644 WonderQ-Admin/data/generated-products.json delete mode 100644 WonderQ-MiniAPP/src/components/MineProductRow.vue delete mode 100644 WonderQ-MiniAPP/src/generated-products.json delete mode 100644 WonderQ-MiniAPP/src/lib/searchFilters.ts delete mode 100644 WonderQ-MiniAPP/src/pages/detail/components/DetailActionBar.vue delete mode 100644 WonderQ-MiniAPP/src/pages/detail/components/DetailHero.vue delete mode 100644 WonderQ-MiniAPP/src/pages/detail/components/DetailInfoCard.vue delete mode 100644 WonderQ-MiniAPP/src/pages/detail/components/DetailMediaGallery.vue delete mode 100644 WonderQ-MiniAPP/src/pages/detail/components/DetailOverviewCard.vue delete mode 100644 WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts delete mode 100644 WonderQ-MiniAPP/src/pages/detail/index.vue delete mode 100644 WonderQ-MiniAPP/tests/searchFilters.test.ts diff --git a/AGENTS.md b/AGENTS.md index e7348ab..121f5d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ WonderQ 是三端分离的旅行产品项目集合,当前由三个独立子项目组成: - `WonderQ-MiniAPP`:万趣旅行移动端前台,面向 H5 与微信小程序,技术栈为 uni-app、Vue 3、TypeScript、TailwindCSS。 -- `WonderQ-Admin-UI`:后台管理前端,用于维护首页结构、目的地、线路商品、活动入口、页面模块和客户需求线索,技术栈为 Vite、React、TypeScript、Tailwind CSS 4、shadcn/ui 风格组件。 +- `WonderQ-Admin-UI`:后台管理前端,用于维护首页结构、目的地、线路商品、页面模块和客户需求线索,技术栈为 Vite、React、TypeScript、Tailwind CSS 4、shadcn/ui 风格组件。 - `WonderQ-Admin`:后端 API 服务,为 MiniAPP 提供 Public API,为后台管理端提供 Admin API,技术栈为 Python、FastAPI、SQLAlchemy 2、Alembic、PostgreSQL、JWT、Pydantic。 核心开发原则是页面驱动开发:业务围绕 MiniAPP/H5 前台页面和运营配置场景展开;后台负责配置页面数据;后端负责业务逻辑、鉴权、数据持久化和接口;MiniAPP 负责展示与交互。 @@ -98,7 +98,6 @@ WonderQ-MiniAPP/ │ ├─ manifest.json │ ├─ pages.json │ ├─ content.ts -│ ├─ generated-products.json │ ├─ components/ │ ├─ lib/ │ └─ pages/ @@ -107,11 +106,11 @@ WonderQ-MiniAPP/ └─ scripts/ ``` -- `src/pages/`:首页、目的地、搜索、活动、需求、权益、预订、详情和我的页面。 -- `src/components/`:复用 Vue SFC 组件,如顶部栏、底部导航、产品卡、客服弹层、空状态和留资表单。 -- `src/lib/api.ts`:Public API 客户端与类型定义,访问 `/api/public/site-config`、`/api/public/products`、`/api/public/leads`。 +- `src/pages/`:首页、目的地、需求、权益和我的页面。 +- `src/components/`:复用 Vue SFC 组件,如顶部栏、底部导航、客服弹层、空状态和留资表单。 +- `src/lib/api.ts`:Public API 客户端与类型定义,访问 `/api/public/site-config`、`/api/public/leads`。 - `src/lib/data.ts`、`src/lib/store.ts`、`src/lib/navigation.ts`、`src/lib/support.ts`:业务数据归一化、共享状态、页面跳转和客服联系封装。 -- `src/content.ts`、`src/generated-products.json`:前台静态兜底内容与产品数据。 +- `src/content.ts`:前台静态兜底内容。 - `public/assets/`:前台图片资源。 ### WonderQ-Admin-UI @@ -140,7 +139,6 @@ WonderQ-Admin-UI/ - `src/App.tsx`:后台应用壳、登录态、侧边导航和全局布局。 - `src/api.ts`:Admin API 类型、Token 管理、鉴权头和请求封装。 - `src/pages/structure/StructurePage.tsx`:首页、目的地页、需求页等页面模块配置主界面。 -- `src/pages/products/ProductsPage.tsx`:线路商品维护。 - `src/pages/leads/LeadsPage.tsx`:需求线索查询、筛选和状态流转。 - `src/components/ui/`:Button、Card、Input、Textarea、Badge、Alert、Switch、NativeSelect、Separator 等基础组件。 - `src/styles.css`:Tailwind 入口、设计变量、后台工作台、表格/表单和移动端适配。 @@ -164,7 +162,7 @@ WonderQ-Admin/ │ ├─ admin.py │ └─ shared.py ├─ alembic/ -├─ data/generated-products.json +├─ data/ ├─ tests/ ├─ Dockerfile ├─ docker-compose.yml @@ -179,7 +177,7 @@ WonderQ-Admin/ - `app/models.py`、`app/schemas.py`、`app/serializers.py`:ORM 模型、Pydantic 校验和响应序列化。 - `app/auth.py`:JWT 与后台鉴权。 - `app/routers/public.py`、`app/routers/admin.py`、`app/routers/shared.py`:Public API、Admin API 和共享查询。 -- `app/content.py`、`app/seed.py`、`data/generated-products.json`:初始化内容、重置命令和迁移数据源。 +- `app/content.py`、`app/seed.py`:初始化内容、重置命令和迁移数据源。 - `alembic/`、`Dockerfile`、`docker-compose.yml`:数据库迁移与部署入口。 ## 7. 启动方式 @@ -249,8 +247,8 @@ yarn dev:mp-weixin - Public API 与 Admin API 分离:MiniAPP 只访问 `/api/public/...`,Admin UI 只访问 `/api/admin/...`。 - 保持现有 API 路径和响应结构兼容;不要为了统一风格擅自改路径、改字段名或改响应包裹结构。 -- 接口设计优先 RESTful 风格,如 `GET /api/admin/products`、`POST /api/admin/products`、`PATCH /api/admin/products/{id}`。 -- 禁止新增 `/getBanner`、`/saveProduct`、`/updateConfig` 这类动作式路径,除非已有契约明确要求。 +- 接口设计优先 RESTful 风格,如 `GET /api/admin/site-config`、`PATCH /api/admin/site-config/{module}/{id}`。 +- 禁止新增 `/getBanner`、`/saveConfig`、`/updateConfig` 这类动作式路径,除非已有契约明确要求。 - TypeScript 与 API 响应字段保持 camelCase;后端内部可使用 snake_case,但序列化给前端时遵守契约。 - 外部输入必须通过 Pydantic schema 或前端表单校验;后端不能信任前端传参。 - 错误响应不得泄露内部异常、SQL、环境变量、Token、密钥或敏感业务数据。 @@ -306,9 +304,9 @@ yarn dev:mp-weixin ### `WonderQ-MiniAPP` - 不要在本子项目重新添加后端、数据库、ORM、Docker Compose、后台认证或管理后台页面。 -- `src/lib/api.ts` 的公共 API 路径保持 `/api/public/site-config`、`/api/public/products`、`/api/public/leads`,除非后端契约已同步变更。 +- `src/lib/api.ts` 的公共 API 路径保持 `/api/public/site-config`、`/api/public/leads`,除非后端契约已同步变更。 - `vite.config.ts`、`manifest.json` 的 `/api` 代理只服务本地开发,不作为生产后端配置。 -- 不要随意改动 `src/generated-products.json`、`public/assets/source/` 中的采集/生成内容,除非任务专门针对这些资源。 +- 不要随意改动 `public/assets/source/` 中的采集内容,除非任务专门针对这些资源。 ### `WonderQ-Admin-UI` @@ -330,7 +328,7 @@ yarn dev:mp-weixin - `app/models.py`、`alembic/versions/*`:数据库结构和迁移。 - `app/auth.py`:后台鉴权逻辑。 - `app/routers/admin.py`、`app/routers/public.py`:核心 API 行为。 -- `app/seed.py`、`app/content.py`、`data/generated-products.json`:初始化内容和迁移数据源。 +- `app/seed.py`、`app/content.py`:初始化内容和迁移数据源。 - `Dockerfile`、`docker-compose.yml`:部署入口。 - `requirements.txt`、`pyproject.toml`:依赖和测试配置。 diff --git a/README.md b/README.md index 70e8fc1..eb3a990 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ python -m uvicorn app.main:app --host 0.0.0.0 --port 4000 --reload - `http://localhost:4000/health` - `http://localhost:4000/docs` -首次初始化空库时才执行 `python -m app.seed`。该命令可能重置站点、商品、目的地、活动和媒体内容,已有开发数据时不要重复执行。 +首次初始化空库时才执行 `python -m app.seed`。该命令可能重置站点、商品、目的地和媒体内容,已有开发数据时不要重复执行。 ### 2. 启动管理后台 diff --git a/WonderQ-Admin-UI/src/api.ts b/WonderQ-Admin-UI/src/api.ts index 2a30ee5..6127ed6 100644 --- a/WonderQ-Admin-UI/src/api.ts +++ b/WonderQ-Admin-UI/src/api.ts @@ -1,30 +1,5 @@ import { notifyAuthExpired, shouldExpireAdminSession } from "@/lib/auth-session"; -export type Dashboard = { - stats: { - productCount: number; - publishedProductCount: number; - destinationCount: number; - newLeadCount: number; - leadCount: number; - campaignCount: number; - }; - recentLeads: Lead[]; -}; - -export type Destination = { - id: string; - name: string; - slug: string; - region?: string | null; - image?: string | null; - isHot: boolean; - isActive: boolean; - sortOrder: number; - aliases?: { id: string; alias: string }[]; - _count?: { products: number }; -}; - export type DestinationHero = { id: string; title: string; @@ -36,17 +11,6 @@ export type DestinationHero = { updatedAt?: string; }; -export type DestinationRegion = { - id: string; - label: string; - keyword?: string | null; - spots?: string | null; - isActive: boolean; - sortOrder: number; - createdAt?: string; - updatedAt?: string; -}; - export type DemandHero = { id: string; title: string; @@ -84,74 +48,6 @@ export type DemandForm = { updatedAt?: string; }; -export type DemandRecommendation = { - id: string; - title: string; - subtitle?: string | null; - productIds: string[]; - isActive: boolean; - sortOrder: number; - createdAt?: string; - updatedAt?: string; -}; - -export type Product = { - id: string; - sourceId?: number | null; - title: string; - subtitle?: string | null; - priceAmount?: number | null; - priceUnit: string; - tags: string[]; - coverImage?: string | null; - summary?: string | null; - images?: Array<{ id?: string; url: string; alt?: string | null; sortOrder: number }>; - detailSections?: ProductDetailSection[] | null; - status: "draft" | "published" | "archived"; - sortWeight: number; - updatedAt: string; - destination?: Destination | null; - destinationId?: string | null; -}; - -export type ProductDetailBlock = - | { type: "text"; text: string } - | { type: "image"; url: string; alt?: string | null }; - -export type ProductDetailSection = { - key: string; - label: string; - title?: string | null; - blocks: ProductDetailBlock[]; -}; - -export type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid"; - -export type Lead = { - id: string; - destination?: string | null; - phone: string; - travelDate?: string | null; - peopleCount?: number | null; - budgetMin?: number | null; - budgetMax?: number | null; - note?: string | null; - sourcePage?: string | null; - status: LeadStatus; - createdAt: string; - sourceProduct?: { id: string; title: string } | null; - assignedUser?: { id: string; name: string } | null; -}; - -export type LeadListParams = { - status?: LeadStatus; - sourcePage?: string; - keyword?: string; - createdFrom?: string; - createdTo?: string; - take?: number; -}; - export type MediaAsset = { id: string; url: string; @@ -163,49 +59,6 @@ export type MediaAsset = { updatedAt?: string; }; -export type Campaign = { - id: string; - slug: string; - title: string; - description: string | null; - coverImage: string | null; - priceAmount: number | null; - priceUnit: string | null; - tags: string[]; - status: "draft" | "published"; - startsAt: string | null; - endsAt: string | null; - createdAt?: string; - updatedAt?: string; -}; - -export type RouteSection = { - id: string; - title: string; - subtitle?: string | null; - productIds: string[]; - isActive: boolean; - sortOrder: number; - createdAt?: string; - updatedAt?: string; -}; - -export type HotelGroup = { - id: string; - title: string; - description: string | null; - image: string | null; - coverImage?: string | null; - priceAmount?: number | null; - priceUnit?: string | null; - tags?: string[]; - status?: "draft" | "published"; - isActive: boolean; - sortOrder: number; - createdAt?: string; - updatedAt?: string; -}; - export type VehicleOption = { id: string; title: string; @@ -219,38 +72,20 @@ export type VehicleOption = { export type SiteConfig = { heroSlides: Array<{ id: string; title: string; kicker: string | null; image: string | null; isActive: boolean; sortOrder: number; createdAt?: string; updatedAt?: string }>; - destinations: Destination[]; destinationHero: DestinationHero[]; - destinationRegions: DestinationRegion[]; demandHero: DemandHero[]; demandFeatureCards: DemandFeatureCard[]; demandForm: DemandForm[]; - demandRecommendations: DemandRecommendation[]; - map: Array<{ id: string; image: string | null; isActive: boolean; createdAt?: string; updatedAt?: string }>; - themes: Array<{ id: string; label: string; image: string; targetType?: string | null; targetValue?: string | null; isActive: boolean }>; - campaigns: Campaign[]; - routeSections: RouteSection[]; - hotelGroups: HotelGroup[]; vehicleOptions: VehicleOption[]; - ctaBanners: Array<{ id: string; alt: string; image: string; targetType: string; targetValue?: string | null; isActive: boolean; sortOrder: number; createdAt?: string; updatedAt?: string }>; }; export type SiteModule = | "heroSlides" - | "destinations" | "destinationHero" - | "destinationRegions" | "demandHero" | "demandFeatureCards" | "demandForm" - | "demandRecommendations" - | "map" - | "themes" - | "campaigns" - | "routeSections" - | "hotelGroups" - | "vehicleOptions" - | "ctaBanners"; + | "vehicleOptions"; export type SiteConfigItem = SiteConfig[SiteModule][number]; type SiteConfigItemResponse = SiteConfigItem | { item: SiteConfigItem }; @@ -258,19 +93,8 @@ export type SiteItemPatch = { title?: string; subtitle?: string | null; kicker?: string | null; - name?: string; - slug?: string; - region?: string | null; - label?: string; - keyword?: string | null; - spots?: string | null; - alt?: string; image?: string | null; description?: string | null; - coverImage?: string | null; - priceAmount?: number | null; - priceUnit?: string | null; - tags?: string[]; steps?: string[]; destinationLabel?: string; destinationPlaceholder?: string | null; @@ -282,28 +106,8 @@ export type SiteItemPatch = { chips?: string[]; targetType?: string | null; targetValue?: string | null; - isHot?: boolean; isActive?: boolean; sortOrder?: number; - productIds?: string[]; - status?: "draft" | "published"; - startsAt?: string | null; - endsAt?: string | null; -}; - -export type ProductInput = { - title: string; - subtitle?: string; - destinationId?: string | null; - priceAmount?: number | null; - priceUnit?: string; - tags: string[]; - coverImage?: string | null; - summary?: string | null; - images?: Array<{ url: string; alt?: string | null; sortOrder: number }>; - detailSections?: ProductDetailSection[]; - status: "draft" | "published" | "archived"; - sortWeight: number; }; const API_BASE = import.meta.env.VITE_API_BASE_URL ?? ""; @@ -355,33 +159,6 @@ export async function login(email: string, password: string) { }); } -export async function getDashboard() { - return request("/api/admin/dashboard"); -} - -export async function getProducts(keyword = "") { - const search = keyword ? `?keyword=${encodeURIComponent(keyword)}` : ""; - return request<{ items: Product[] }>(`/api/admin/products${search}`); -} - -export async function createProduct(input: ProductInput) { - return request("/api/admin/products", { - method: "POST", - body: JSON.stringify(input), - }); -} - -export async function updateProduct(id: string, input: Partial) { - return request(`/api/admin/products/${id}`, { - method: "PATCH", - body: JSON.stringify(input), - }); -} - -export async function getDestinations() { - return request<{ items: Destination[] }>("/api/admin/destinations"); -} - export async function uploadMediaAsset(file: File, group = "site-config") { const form = new FormData(); form.set("file", file); @@ -444,25 +221,6 @@ export async function reorderSiteConfigItems(module: SiteModule, itemIds: string }); } -export async function getLeads(params: LeadListParams = {}) { - const search = new URLSearchParams(); - if (params.status) search.set("status", params.status); - if (params.sourcePage) search.set("sourcePage", params.sourcePage); - if (params.keyword) search.set("keyword", params.keyword); - if (params.createdFrom) search.set("createdFrom", params.createdFrom); - if (params.createdTo) search.set("createdTo", params.createdTo); - if (params.take) search.set("take", String(params.take)); - const query = search.toString(); - return request<{ items: Lead[] }>(`/api/admin/leads${query ? `?${query}` : ""}`); -} - -export async function updateLeadStatus(id: string, status: Lead["status"]) { - return request(`/api/admin/leads/${id}/status`, { - method: "PATCH", - body: JSON.stringify({ status }), - }); -} - export async function publishSite() { return request<{ id: string; title: string; publishedAt: string }>("/api/admin/publish", { method: "POST", @@ -471,7 +229,15 @@ export async function publishSite() { } export async function resetGuizhouContent() { - return request<{ heroSlides: number; destinations: number; themes: number; ctaBanners: number; routeSections?: number; products: number }>("/api/admin/reset-guizhou-content", { + return request<{ + heroSlides: number; + destinationHero: number; + vehicleOptions: number; + demandHero: number; + demandFeatureCards: number; + demandForm: number; + siteVersionId: string; + }>("/api/admin/reset-guizhou-content", { method: "POST", body: JSON.stringify({}), }); diff --git a/WonderQ-Admin-UI/src/components/admin/AdminShell.tsx b/WonderQ-Admin-UI/src/components/admin/AdminShell.tsx index 4f0f264..35caaa3 100644 --- a/WonderQ-Admin-UI/src/components/admin/AdminShell.tsx +++ b/WonderQ-Admin-UI/src/components/admin/AdminShell.tsx @@ -1,61 +1,48 @@ -import { useEffect, useMemo, useState } from "react"; -import { Blocks, Home, LogOut, MapPin, Package, UserRoundCheck } from "lucide-react"; +import { useMemo, useState } from "react"; +import { BriefcaseBusiness, Home, LogOut, MapPin } from "lucide-react"; -import { clearToken, getDestinations } from "@/api"; -import type { Destination } from "@/api"; +import { clearToken } from "@/api"; +import { EmptyState } from "@/components/admin/EmptyState"; import { ToastStack } from "@/components/admin/ToastStack"; import { Button } from "@/components/ui/button"; -import { LeadsPage } from "@/pages/leads/LeadsPage"; -import { ProductsPage } from "@/pages/products/ProductsPage"; import { StructurePage } from "@/pages/structure/StructurePage"; import type { Tab, Toast } from "@/types/admin"; -const sidebarGroups: { id: string; label: string; items: { id: Tab; label: string; icon: typeof Home; description: string }[] }[] = [ - { - id: "workspace", - label: "工作台", - items: [{ id: "structure", label: "维护地图", icon: Blocks, description: "前台结构总览" }], - }, +const sidebarGroups: { + id: string; + label: string; + items: { id: Tab; label: string; icon: typeof Home; description: string }[]; +}[] = [ { id: "frontend", label: "前台页面", items: [ - { id: "home", label: "小程序首页", icon: Home, description: "轮播、主题、CTA" }, - { id: "destinations", label: "目的地页", icon: MapPin, description: "主视觉、热门、省内" }, - { id: "products", label: "商品维护", icon: Package, description: "列表、卡片、详情" }, + { id: "home", label: "首页", icon: Home, description: "" }, + { id: "play", label: "玩法", icon: MapPin, description: "" }, + { id: "butler", label: "管家", icon: BriefcaseBusiness, description: "" }, ], }, - { - id: "operations", - label: "业务处理", - items: [{ id: "leads", label: "需求线索", icon: UserRoundCheck, description: "表单和跟进" }], - }, ]; const tabs = sidebarGroups.flatMap((group) => group.items); -const hiddenTabs: Partial> = { - leadList: { label: "线索查询跟进", description: "表单查询和状态流转" }, -}; export function AdminShell({ onLogout }: { onLogout: () => void }) { - const [activeTab, setActiveTab] = useState("structure"); - const [destinations, setDestinations] = useState([]); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [activeTab, setActiveTab] = useState("home"); const [toasts, setToasts] = useState([]); - useEffect(() => { - getDestinations() - .then((result) => setDestinations(result.items)) - .catch(() => setDestinations([])); - }, [activeTab]); - - useEffect(() => { - setHasUnsavedChanges(false); - }, [activeTab]); - - const activeMeta = useMemo(() => tabs.find((tab) => tab.id === activeTab) ?? hiddenTabs[activeTab], [activeTab]); + const activeMeta = useMemo( + () => tabs.find((tab) => tab.id === activeTab), + [activeTab], + ); const activeLabel = activeMeta?.label ?? ""; const activeDescription = activeMeta?.description ?? ""; + const activeTopbarDescription = [ + activeDescription, + "结构化维护", + "内存开发模式", + ] + .filter(Boolean) + .join(" · "); const notify = (toast: Omit) => { const id = `toast-${Date.now()}-${Math.random().toString(16).slice(2)}`; @@ -74,12 +61,15 @@ export function AdminShell({ onLogout }: { onLogout: () => void }) {
); } diff --git a/WonderQ-Admin-UI/src/components/admin/route-sections/RouteProductQuickCreateForm.tsx b/WonderQ-Admin-UI/src/components/admin/route-sections/RouteProductQuickCreateForm.tsx deleted file mode 100644 index 6b2c624..0000000 --- a/WonderQ-Admin-UI/src/components/admin/route-sections/RouteProductQuickCreateForm.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { Plus, X } from "lucide-react"; -import { MutableRefObject, useEffect, useState } from "react"; - -import type { Product, ProductInput } from "@/api"; -import { AdminDisclosure } from "@/components/admin/AdminDisclosure"; -import { SingleImageUploader } from "@/components/admin/SingleImageUploader"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Switch } from "@/components/ui/switch"; -import { Textarea } from "@/components/ui/textarea"; - -type QuickRouteDraft = { - title: string; - summary: string; - destinationName: string; - priceAmount: number | null; - priceUnit: string; - tags: string[]; - coverImage: string; - published: boolean; - sortWeight: number; -}; - -function createEmptyDraft(): QuickRouteDraft { - return { - title: "", - summary: "", - destinationName: "", - priceAmount: null, - priceUnit: "起/人", - tags: ["", ""], - coverImage: "", - published: true, - sortWeight: 0, - }; -} - -function createDraftFromProduct(product?: Product | null): QuickRouteDraft { - if (!product) return createEmptyDraft(); - - const tags = product.tags.length ? product.tags.slice(0, 3) : ["", ""]; - - return { - title: product.title, - summary: product.summary ?? "", - destinationName: product.subtitle ?? product.destination?.name ?? "", - priceAmount: product.priceAmount ?? null, - priceUnit: product.priceUnit || "起/人", - tags: tags.length === 1 ? [...tags, ""] : tags, - coverImage: product.coverImage ?? "", - published: product.status === "published", - sortWeight: product.sortWeight ?? 0, - }; -} - -export function RouteProductQuickCreateForm({ - product, - onCreate, - onUpdate, - onSubmitRef, -}: { - product?: Product | null; - onCreate: (input: ProductInput) => Promise; - onUpdate: (productId: string, input: ProductInput) => Promise; - onSubmitRef: MutableRefObject<(() => Promise) | null>; -}) { - const [draft, setDraft] = useState(() => createDraftFromProduct(product)); - const editing = Boolean(product); - - const submit = async () => { - const title = draft.title.trim(); - if (!title) return; - - const input: ProductInput = { - title, - subtitle: draft.destinationName.trim(), - destinationId: null, - priceAmount: draft.priceAmount, - priceUnit: draft.priceUnit.trim() || "起/人", - tags: draft.tags.map((tag) => tag.trim()).filter(Boolean).slice(0, 3), - coverImage: draft.coverImage.trim() || null, - summary: draft.summary.trim(), - images: product?.images ?? [], - detailSections: product?.detailSections ?? [], - status: draft.published ? "published" : "draft", - sortWeight: draft.sortWeight, - }; - - if (editing && product) { - await onUpdate(product.id, input); - return; - } - - await onCreate(input); - setDraft(createEmptyDraft()); - }; - - useEffect(() => { - setDraft(createDraftFromProduct(product)); - }, [product?.id, product?.updatedAt]); - - useEffect(() => { - onSubmitRef.current = submit; - return () => { - onSubmitRef.current = null; - }; - }, [draft, product?.id, product?.updatedAt, onSubmitRef]); - - return ( -
- - -