移除 H5 商品兜底并补充内容发布流程
This commit is contained in:
12
README.md
12
README.md
@@ -8,3 +8,15 @@ Mobile H5, admin, and API workspace for the WonderQ MiniAPP project.
|
|||||||
- `npm run dev:admin` starts the admin app.
|
- `npm run dev:admin` starts the admin app.
|
||||||
- `npm run dev:api` starts the API service.
|
- `npm run dev:api` starts the API service.
|
||||||
- `npm run build:all` builds all workspaces.
|
- `npm run build:all` builds all workspaces.
|
||||||
|
|
||||||
|
## 后端内容发布
|
||||||
|
|
||||||
|
商品数据的正式源文件是 `src/generated-products.json`。后端部署时请从仓库根目录执行内容发布流程,不要把商品数据重新打进 H5:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:content:release -- validate
|
||||||
|
npm run db:content:release -- plan
|
||||||
|
npm run db:content:release -- apply
|
||||||
|
```
|
||||||
|
|
||||||
|
`apply` 会把商品、首页配置、活动和媒体引用写入生产数据库,并在 `artifacts/` 生成可回滚快照。需要回滚时,使用发布命令输出的 `rollback` 命令。
|
||||||
|
|||||||
64
src/App.tsx
64
src/App.tsx
@@ -47,7 +47,6 @@ import {
|
|||||||
type PublicSearchPageConfig,
|
type PublicSearchPageConfig,
|
||||||
type PublicSiteConfig,
|
type PublicSiteConfig,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import fallbackProductsJson from "./generated-products.json";
|
|
||||||
import {
|
import {
|
||||||
bottomCtas as fallbackBottomCtas,
|
bottomCtas as fallbackBottomCtas,
|
||||||
dealCards,
|
dealCards,
|
||||||
@@ -255,54 +254,6 @@ type SwipeState = {
|
|||||||
axis: "horizontal" | "vertical" | null;
|
axis: "horizontal" | "vertical" | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SourceProduct = {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
price: string;
|
|
||||||
priceUnit?: string | null;
|
|
||||||
pricingTiers?: ProductPricingTier[] | null;
|
|
||||||
tags: string[];
|
|
||||||
image: string;
|
|
||||||
images?: Array<{ url: string; alt?: string | null; sortOrder?: number }>;
|
|
||||||
summary?: string | null;
|
|
||||||
destinationName?: string | null;
|
|
||||||
durationDays?: number | null;
|
|
||||||
durationNights?: number | null;
|
|
||||||
departureDates?: string[];
|
|
||||||
remainingSpots?: number | null;
|
|
||||||
recommendation?: string | null;
|
|
||||||
keyFacts?: ProductKeyFact[] | null;
|
|
||||||
contentBlocks?: ProductContentBlock[] | null;
|
|
||||||
detailSections?: ProductDetailSection[] | null;
|
|
||||||
status?: string;
|
|
||||||
sortWeight?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeFallbackProduct(product: SourceProduct): Product {
|
|
||||||
return {
|
|
||||||
id: product.id,
|
|
||||||
title: product.title,
|
|
||||||
price: product.price,
|
|
||||||
priceUnit: product.priceUnit,
|
|
||||||
pricingTiers: product.pricingTiers,
|
|
||||||
tags: product.tags,
|
|
||||||
image: product.image,
|
|
||||||
summary: product.summary,
|
|
||||||
destinationName: product.destinationName,
|
|
||||||
durationDays: product.durationDays,
|
|
||||||
durationNights: product.durationNights,
|
|
||||||
departureDates: product.departureDates,
|
|
||||||
remainingSpots: product.remainingSpots,
|
|
||||||
recommendation: product.recommendation,
|
|
||||||
images: product.images?.map((image, index) => ({ ...image, sortOrder: image.sortOrder ?? index })),
|
|
||||||
keyFacts: product.keyFacts,
|
|
||||||
contentBlocks: product.contentBlocks,
|
|
||||||
detailSections: product.detailSections,
|
|
||||||
status: product.status,
|
|
||||||
sortWeight: product.sortWeight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallbackNumberFromApiId(apiId: string, index: number) {
|
function fallbackNumberFromApiId(apiId: string, index: number) {
|
||||||
const digits = apiId.match(/\d+/)?.[0];
|
const digits = apiId.match(/\d+/)?.[0];
|
||||||
return digits ? Number(digits) : 10000 + index;
|
return digits ? Number(digits) : 10000 + index;
|
||||||
@@ -470,7 +421,7 @@ const fallbackData: AppData = {
|
|||||||
destinations: fallbackDestinations,
|
destinations: fallbackDestinations,
|
||||||
themeCards: fallbackThemeCards,
|
themeCards: fallbackThemeCards,
|
||||||
bottomCtas: fallbackBottomCtas,
|
bottomCtas: fallbackBottomCtas,
|
||||||
products: (fallbackProductsJson as SourceProduct[]).map(normalizeFallbackProduct),
|
products: [],
|
||||||
searchPage: fallbackSearchPageConfig,
|
searchPage: fallbackSearchPageConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -481,12 +432,7 @@ function useAppData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getProducts(products: Product[]) {
|
function getProducts(products: Product[]) {
|
||||||
return products.length ? products : fallbackData.products;
|
return products;
|
||||||
}
|
|
||||||
|
|
||||||
function getProductAt(products: Product[], index: number) {
|
|
||||||
const list = getProducts(products);
|
|
||||||
return list[index] ?? list[0];
|
|
||||||
}
|
}
|
||||||
const keywordAliases: Record<string, string[]> = {
|
const keywordAliases: Record<string, string[]> = {
|
||||||
贵州: ["贵州", "贵阳", "黄果树", "荔波", "西江", "梵净山", "万峰林"],
|
贵州: ["贵州", "贵阳", "黄果树", "荔波", "西江", "梵净山", "万峰林"],
|
||||||
@@ -2515,7 +2461,7 @@ function SearchResultsPage({
|
|||||||
{contentLoadState !== "ready" ? (
|
{contentLoadState !== "ready" ? (
|
||||||
<div className={`result-sync-status ${contentLoadState}`} role="status">
|
<div className={`result-sync-status ${contentLoadState}`} role="status">
|
||||||
<span aria-hidden="true" />
|
<span aria-hidden="true" />
|
||||||
<p>{contentLoadState === "loading" ? "正在同步最新线路…" : "网络暂不可用,已显示本地线路"}</p>
|
<p>{contentLoadState === "loading" ? "正在同步最新线路…" : "网络暂不可用,暂时无法加载线路"}</p>
|
||||||
{contentLoadState === "offline" ? <button onClick={onRetryContent}>重试</button> : null}
|
{contentLoadState === "offline" ? <button onClick={onRetryContent}>重试</button> : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -3686,14 +3632,14 @@ export default function App() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setAppData({
|
setAppData({
|
||||||
...normalizeSiteConfig(siteConfig, fallbackData),
|
...normalizeSiteConfig(siteConfig, fallbackData),
|
||||||
products: products.length ? products.map(normalizeApiProduct) : fallbackData.products,
|
products: products.map(normalizeApiProduct),
|
||||||
});
|
});
|
||||||
setContentLoadState("ready");
|
setContentLoadState("ready");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setContentLoadState("offline");
|
setContentLoadState("offline");
|
||||||
console.info("Public API unavailable, using bundled H5 content.", error);
|
console.info("Public API unavailable; product content remains empty until the API is available.", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user