This commit is contained in:
2026-07-24 12:30:50 +08:00
4 changed files with 19 additions and 61 deletions

View File

@@ -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` 命令。

View File

@@ -43,7 +43,7 @@ const baseConfig: UserConfigExport = {
compiler: "webpack5",
defineConstants: {
"process.env.TARO_APP_API_BASE_URL": JSON.stringify(process.env.TARO_APP_API_BASE_URL || DEFAULT_API_BASE_URL),
"process.env.TARO_APP_H5_URL": JSON.stringify(process.env.TARO_APP_H5_URL ?? "http://127.0.0.1:5173"),
"process.env.TARO_APP_H5_URL": JSON.stringify(process.env.TARO_APP_H5_URL ?? "https://wanderq.nianxx.com"),
},
alias: {
// Taro 4.2.1 requires React 18. Keep the mini program's renderer on the

View File

@@ -1,6 +1,6 @@
import { WebView } from "@tarojs/components";
const H5_URL = String(process.env.TARO_APP_H5_URL ?? "http://127.0.0.1:5173");
const H5_URL = String(process.env.TARO_APP_H5_URL ?? "https://wanderq.nianxx.com");
const H5_CACHE_VERSION = "ui-20260723-tab-seam-v7";
const H5_SRC = `${H5_URL}${H5_URL.includes("?") ? "&" : "?"}v=${H5_CACHE_VERSION}`;

View File

@@ -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 () => {