Merge branch 'main' of https://git.nianxx.cn/wangxuming/WanderQ-MiniAPP2.0
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` 命令。
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const baseConfig: UserConfigExport = {
|
|||||||
compiler: "webpack5",
|
compiler: "webpack5",
|
||||||
defineConstants: {
|
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_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: {
|
alias: {
|
||||||
// Taro 4.2.1 requires React 18. Keep the mini program's renderer on the
|
// Taro 4.2.1 requires React 18. Keep the mini program's renderer on the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { WebView } from "@tarojs/components";
|
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_CACHE_VERSION = "ui-20260723-tab-seam-v7";
|
||||||
const H5_SRC = `${H5_URL}${H5_URL.includes("?") ? "&" : "?"}v=${H5_CACHE_VERSION}`;
|
const H5_SRC = `${H5_URL}${H5_URL.includes("?") ? "&" : "?"}v=${H5_CACHE_VERSION}`;
|
||||||
|
|
||||||
|
|||||||
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