移除 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:api` starts the API service.
|
||||
- `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 PublicSiteConfig,
|
||||
} from "./api";
|
||||
import fallbackProductsJson from "./generated-products.json";
|
||||
import {
|
||||
bottomCtas as fallbackBottomCtas,
|
||||
dealCards,
|
||||
@@ -255,54 +254,6 @@ type SwipeState = {
|
||||
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) {
|
||||
const digits = apiId.match(/\d+/)?.[0];
|
||||
return digits ? Number(digits) : 10000 + index;
|
||||
@@ -470,7 +421,7 @@ const fallbackData: AppData = {
|
||||
destinations: fallbackDestinations,
|
||||
themeCards: fallbackThemeCards,
|
||||
bottomCtas: fallbackBottomCtas,
|
||||
products: (fallbackProductsJson as SourceProduct[]).map(normalizeFallbackProduct),
|
||||
products: [],
|
||||
searchPage: fallbackSearchPageConfig,
|
||||
};
|
||||
|
||||
@@ -481,12 +432,7 @@ function useAppData() {
|
||||
}
|
||||
|
||||
function getProducts(products: Product[]) {
|
||||
return products.length ? products : fallbackData.products;
|
||||
}
|
||||
|
||||
function getProductAt(products: Product[], index: number) {
|
||||
const list = getProducts(products);
|
||||
return list[index] ?? list[0];
|
||||
return products;
|
||||
}
|
||||
const keywordAliases: Record<string, string[]> = {
|
||||
贵州: ["贵州", "贵阳", "黄果树", "荔波", "西江", "梵净山", "万峰林"],
|
||||
@@ -2515,7 +2461,7 @@ function SearchResultsPage({
|
||||
{contentLoadState !== "ready" ? (
|
||||
<div className={`result-sync-status ${contentLoadState}`} role="status">
|
||||
<span aria-hidden="true" />
|
||||
<p>{contentLoadState === "loading" ? "正在同步最新线路…" : "网络暂不可用,已显示本地线路"}</p>
|
||||
<p>{contentLoadState === "loading" ? "正在同步最新线路…" : "网络暂不可用,暂时无法加载线路"}</p>
|
||||
{contentLoadState === "offline" ? <button onClick={onRetryContent}>重试</button> : null}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -3686,14 +3632,14 @@ export default function App() {
|
||||
if (cancelled) return;
|
||||
setAppData({
|
||||
...normalizeSiteConfig(siteConfig, fallbackData),
|
||||
products: products.length ? products.map(normalizeApiProduct) : fallbackData.products,
|
||||
products: products.map(normalizeApiProduct),
|
||||
});
|
||||
setContentLoadState("ready");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user