注意: 此教程包含更早的 WooGraphQL 出放, 已不更新 。 有相關的相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相相相關相相相相關相關相相關相相相相關相相相關相相相相關相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相
表格
歡迎收看相關相關相關相關相關相關相關相關相關相關相關相關事項, 有很長的路可走 而現在是時候把收尾接通到您的應用程式上了 为你的顧客創造出一個 有權力 有弹性的購物經驗了
建立產品頁面和墨盒選項
有能顯示出相關產品細節, 這能讓使用者全面了解自己所想得到的產品, 我們將由你來研究如何建立并設計出一個詳細的單一產品頁面来展示出你的產品所特有的地點和特質.
接著再用 Cart 選取程式去提升你的商店功能了 這些選項是平滑而灵活的購物流程的關鍵, 讓用戶有權去有效管理他們的訂單. 有相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相等相當相當相當相當相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相
我們會想出出一個活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活 SessionProvider 在上一個教程中被發展出. 此外, 我們將引入一款新通訊網和路由頁面, 有工具可以提供最上等的、無缝的購物經驗。
需要注意的是,這所教訓所基于的是前几所教訓中引入的概念和代碼,所以要確保您已經歷了之前所有的教訓并自在地掌握了內容.
在這個教程結束后 有一家功能完好 有特色的eCommerce商店可以吸引你的客戶了 因此,不用再多說了 就把相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相關相關相關相關相相關相相相相關相相相關相相相相相相關相相相相相相關相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相
第 1 部分: 建立單一產品頁面
1. 建立 /app/product/[slug]/page.tsx:
// app/产品/[slug]/page.tsx
import { fetchProductBy, ProductIdTypeEnum } from '@/graphql';
import { ShopProduct } from '@/server/ShopProduct';
export interface ProductPageProps {
params: {
slug: string
}
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = params;
const product = await fetchProductBy(slug, ProductIdTypeEnum.SLUG);
if (!slug || !product) return (
<h1>Page not found</h1>
);
return (
<ShopProduct product={product}/>
);
}2. 建立 /server/ShopProduct:
// 伺服器/ShopProduct/ShopProduct.tsx
import { Product, SimpleProduct, VariationAttribute } from '@/graphql';
import { ProductImage } from '@/client/ProductImage';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/ui/tabs';
export interface ShopProductProps {
product: Product;
}
export function ShopProduct(props: ShopProductProps) {
const {
product,
} = props;
const attributes: Record<string, string[]> = (product as SimpleProduct).defaultAttributes?.nodes?.reduce(
(attributesList, attribute) => {
const {
value,
label
} = attribute as VariationAttribute;
const currentAttributes = attributesList[label as string] || [];
return {
...attributesList,
[label as string]: [...currentAttributes, value as string],
};
},
{} as Record<string, string[]>
) || {};
return (
<div className="w-full flex flex-wrap gap-4 max-w-screen-lg mx-auto mb-36 lg:mb-0">
<ProductImage product={product}/>
<div className="basis-full md:basis-1/2 pt-4 px-4 flex flex-col">
<h1 className="font-serif text-2xl font-bold mb-2">{product.name}</h1>
<p className="text-lg font-bold mb-2">{product.shortDescription}</p>
<CartOptions product={product}/>
</div>
<Tabs defaultValue="description" className="w-full px-4">
<TabsList className="w-full md:w-auto">
<TabsTrigger value="description">Description</TabsTrigger>
<TabsTrigger value="attributes">Attributes</TabsTrigger>
<TabsTrigger value="reviews">Reviews</TabsTrigger>
</TabsList>
<TabsContent value="description">
<div dangerouslySetInnerHTML={{ __html: product.description as string }}/>
</TabsContent>
<TabsContent value="attributes">
<ul>
{Object.entries(attributes).map(([label, values]) => (
<li key={label}>
<p><span className="font-serif font-medium">{label}:</span> {values.join(', ')}</p>
</li>
))}
</ul>
</TabsContent>
<TabsContent value="reviews">
<p>To Be Continued...</p>
</TabsContent>
</Tabs>
</div>
);
}
三. 建立 /client/ProductImage:
// 第 二 章
import { Product } from '@/graphql';
import { Image } from '@/ui/Image';
export interface ProductImageProps {
product: Product;
}
export function ProductImage({ product }: ProductImageProps) {
const sourceUrl = product?.image?.sourceUrl;
const altText = product?.image?.altText || '';
if (!sourceUrl) {
return null;
}
return (
<div className="basis-full md:basis-auto grow">
<Image
className="md:rounded-br"
src={sourceUrl}
alt={altText}
ratio={3 / 4}
/>
</div>
);
}
且道. 有一頁有相片和相片 有出自相關報導,
有評論出自此教程外. 就當作作業吧. 就價格和推車可選方案而言 我們下回再做 就跟前兩回相關教學一樣 有新上下文提供商需要相接相接 它能搞出我們所收看出品的收視狀態 有同樣的相關相關資訊相關, 就去搞吧.
第2部分:建立 ProductProvider
1. 建立 /client/ProductProvider:
// 第 二 部 分
import {
useContext,
useReducer,
useEffect,
createContext,
PropsWithChildren
} from 'react';
import get from 'lodash/get';
import {
Product,
ProductTypesEnum,
ProductVariation,
SimpleProduct,
VariableProduct,
} from '@/graphql';
export interface ProductContext {
data: Product|null;
isVariableProduct: boolean;
hasSelectedVariation: boolean;
selectedVariation: ProductVariation | null;
get: (field: keyof ProductVariation | keyof SimpleProduct | keyof VariableProduct) => unknown;
selectVariation: (variation?: ProductVariation) => void;
}
const initialState: ProductContext = {
data: null,
isVariableProduct: false,
hasSelectedVariation: false,
selectedVariation: null,
get: () => null,
selectVariation: () => null,
};
const productContext = createContext<ProductContext>(initialState);
export function useProductContext() {
return useContext(productContext);
}
type ProductAction = {
type: 'SET_PRODUCT';
payload: Product;
} | {
type: 'SET_VARIATION';
payload: ProductVariation|null;
} | {
type: 'SET_IS_VARIABLE_PRODUCT';
payload: boolean;
} | {
type: 'SET_HAS_SELECTED_VARIATION';
payload: boolean;
};
const null_variation = {
id: '',
databaseId: 0,
} as ProductVariation;
function reducer(state: ProductContext, action: ProductAction) {
switch(action.type) {
case 'SET_PRODUCT':
return {
...state,
data: action.payload,
};
case 'SET_VARIATION':
return {
...state,
selectedVariation: action.payload,
};
case 'SET_IS_VARIABLE_PRODUCT':
return {
...state,
isVariableProduct: action.payload,
};
case 'SET_HAS_SELECTED_VARIATION':
return {
...state,
hasSelectedVariation: action.payload,
};
default:
return state;
}
}
const { Provider } = productContext;
export interface ProductProviderProps {
product: Product;
}
export function ProductProvider({ product, children}: PropsWithChildren<ProductProviderProps>) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({
type: 'SET_PRODUCT',
payload: product,
});
if (product.type === ProductTypesEnum.VARIABLE) {
dispatch({
type: 'SET_IS_VARIABLE_PRODUCT',
payload: true,
});
dispatch({
type: 'SET_HAS_SELECTED_VARIATION',
payload: false,
});
dispatch({
type: 'SET_VARIATION',
payload: null_variation,
});
}
}, );
const store = {
...state,
get: (field: keyof ProductVariation | keyof SimpleProduct | keyof VariableProduct) => {
if (!state.data) {
return null;
}
if (state.selectedVariation) {
return get(state.selectedVariation, field as keyof ProductVariation)
|| get(state.data as VariableProduct, field as keyof VariableProduct);
}
return get(state.data as SimpleProduct, field as keyof SimpleProduct);
},
selectVariation: (variation?: ProductVariation) => {
if (!variation) {
dispatch({
type: 'SET_VARIATION',
payload: null_variation,
});
dispatch({
type: 'SET_HAS_SELECTED_VARIATION',
payload: false,
});
return;
}
dispatch({
type: 'SET_VARIATION',
payload: variation,
});
dispatch({
type: 'SET_HAS_SELECTED_VARIATION',
payload: true,
});
},
};
return (
<Provider value={store}>
{children}
</Provider>
)
}
ProductProvider 有活要干. 以分離出品變更并回傳回相關 get 有回復了 有回復了
get 在所選取的變更上取字段名稱回傳字段值。 如果產品不是可變出品 或者沒有選擇可變出品 get 从主產物取出字段值。 有活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活 SessionProvider 在單一頁元件上。
2. 更新 /app/product/[slug]/page.tsx:
// app/产品/[slug]/page.tsx
...
import { ShopProduct } from '/server/ShopProduct';
...
export default async function ProductPage({ params }: ProductPageProps) {
...
return (
<ProductProvider product={product}>
<ShopProduct product={product}/>
</ProductProvider>
);
}但是我們需要做一些改變 在上回教程中所創造出的一些事。 该 SessionProvider 二. 构成部分和 session.ts 需要更新工具檔以處理拖放動作 。 就從一開始 session.ts 檔案。
第3部分:更新 SessionProvider
1. 更新 /utils/session.ts:
// 已啟用/相關
...
type FetchCartResponse = {
cart: Cart;
sessionToken: string;
}
export type CartAction = {
mutation: 'add';
productId: number;
quantity: number;
variationId?: number;
variation?: {
attributeName: string;
attributeValue: string;
}[]
extraData?: string;
} | {
mutation: 'update';
items: { key: string, quantity: number }[];
} | {
mutation: 'remove';
keys: string[];
all?: boolean;
}
export async function updateCart(input: CartAction): Promise<Cart|string> {
const sessionToken = await getSessionToken();
const authToken = await getAuthToken();
let json: FetchCartResponse;
try {
json = await apiCall<FetchCartResponse>(
'/api/cart',
{
method: 'POST',
body: JSON.stringify({
sessionToken,
authToken,
input,
}),
},
);
} catch (error) {
return (error as GraphQLError)?.message || error as string;
}
const { cart } = json;
saveSessionToken(json.sessionToken);
return cart;
}有一點小變化 但是您可能也注意到了 updateCart 二用功能, 此外取取新功能 CartAction 在輸入時,它也向您傳送 POST 要求 /api/cart它并不存在。 就去補償吧
2. 建立 /app/api/cart/route.ts:
import { NextResponse } from 'next/server';
import { print } from 'graphql';
import {
AddToCartMutation,
AddToCartDocument,
UpdateCartItemQuantitiesMutation,
UpdateCartItemQuantitiesDocument,
RemoveItemsFromCartMutation,
RemoveItemsFromCartDocument,
Cart,
getClient,
} from '@/graphql';
import { CartAction } from '@/utils/session';
type RequestBody = {
sessionToken: string;
authToken?: string;
input: CartAction;
}
type GraphQLRequestHeaders = {
Authorization?: string;
'woocommerce-session': string;
}
export async function POST(request: Request) {
try {
const body = await request.json() as RequestBody;
if (!body.sessionToken) {
return NextResponse.json({ errors: { message: 'Session not started' } }, { status: 500 });
}
const headers: GraphQLRequestHeaders = { 'woocommerce-session': `Session ${body.sessionToken}` };
if (body.authToken) {
headers.Authorization = `Bearer ${body.authToken}`;
}
const graphQLClient = getClient();
graphQLClient.setHeaders(headers);
if (!body.input) {
return NextResponse.json({ errors: { message: 'No input provided' } }, { status: 500 });
}
const { mutation, ...input } = body.input;
if (!mutation) {
return NextResponse.json({ errors: { message: 'No mutation provided' } }, { status: 500 });
}
let cart: Cart;
let sessionToken: string|null = null;
let results;
switch (mutation) {
case 'add':
results = await graphQLClient.rawRequest<AddToCartMutation>(
print(AddToCartDocument),
{ ...input },
);
cart = results.data?.addToCart?.cart as Cart;
sessionToken = results.headers.get('woocommerce-session');
break;
case 'update':
results = await graphQLClient.rawRequest<UpdateCartItemQuantitiesMutation>(
print(UpdateCartItemQuantitiesDocument),
{ ...input },
);
cart = results.data?.updateItemQuantities?.cart as Cart;
sessionToken = results.headers.get('woocommerce-session') || body.sessionToken;
break;
case 'remove':
results = await graphQLClient.rawRequest<RemoveItemsFromCartMutation>(
print(RemoveItemsFromCartDocument),
{ ...input },
);
cart = results.data?.removeItemsFromCart?.cart as Cart;
sessionToken = results.headers.get('woocommerce-session') || body.sessionToken;
break;
default:
return NextResponse.json({ errors: { message: 'Invalid mutation provided' } }, { status: 500 });
}
if (!cart || !sessionToken) {
const message = 'No cart or session token returned from WooCommerce';
return NextResponse.json({ errors: { message } }, { status: 500 });
}
return NextResponse.json({ cart, sessionToken });
} catch (err) {
console.log(err);
return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
}
}该 /api/cart 需要 POST 要求 sessionToken 和一 input 在我們的外形上 CartAction如果 authToken 第 二 章 sessionToken。提供相當入數后, POST Callback 試圖執行指定的動作 input 物件并返回結果 cart 會議物件。 有了這個我們可以更新了 SessionProvider有活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活
三. 更新 /client/SessionProvider:
// 第 二 部 分
...
import {
...
CartItem,
MetaData,
VariationAttribute,
} from '@/graphql';
import {
...
updateCart as updateCartApiCall,
CartAction,
} from '@/utils/session';
...
export interface SessionContext {
...
updateCart: (action: CartAction) => Promise<boolean>;
findInCart: (
productId: number,
variationId?: number,
variation?: {
attributeName: string;
attributeValue: string;
}[],
extraData?: string,
) => CartItem|undefined;
}
const initialContext: SessionContext = {
...
updateCart: (action: CartAction) => new Promise((resolve) => { resolve(false); }),
findInCart: (
productId: number,
variationId?: number,
variation?: {
attributeName: string;
attributeValue: string;
}[],
extraData?: string,
) => undefined,
};
...
/**
* Checks if product matches the provided cart/registry item.
*
* @param {number} productId Item product ID.
* @param {number} variationId Item variation ID.
* @param {string} extraData Item metadata JSON string.
* @returns
*/
const cartItemSearch = (
productId: number,
variationId?: number,
variationData?: {
attributeName: string;
attributeValue: string;
}[],
extraData?: string,
skipMeta = false,
) => ({
product,
variation,
extraData:
existingExtraData = [],
}: CartItem) => {
if (product?.node?.databaseId && productId !== product.node.databaseId) {
return false;
}
if (
variation?.node?.databaseId
&& variationId !== variation.node.databaseId
) {
return false;
}
if (variationData?.length && !variation?.attributes?.length) {
return false;
}
if (variationData?.length && variation?.attributes?.length) {
const variationAttributes = variation.attributes as VariationAttribute[];
const found = variationData
.filter(({ attributeName, attributeValue }) => !!variationAttributes?.find(
({ label, value }) => (label as string).toLowerCase() === attributeName && value === attributeValue,
))
.length;
if (!found) return false;
}
if (skipMeta) {
return true;
}
if (existingExtraData?.length && !extraData) {
return false;
}
if (!!extraData && typeof extraData === 'string') {
const decodeMeta = JSON.parse(extraData);
let found = false;
Object.entries(decodeMeta).forEach(([targetKey]) => {
found = !!(existingExtraData as MetaData[])?.find(
({ key, value }) => key === targetKey && value === `${decodeMeta[targetKey]}`,
);
});
if (!found) {
return false;
}
}
return true;
};
...
export function SessionProvider({ children }: PropsWithChildren) {
...
// 行程推車動作回應 。
const setCart = (cart: Cart|string) => {
if (typeof cart === 'string') {
toast({
title: 'Cart Action Error',
description: 'Cart mutation failed.',
variant: 'destructive'
});
dispatch({
type: 'UPDATE_STATE',
payload: { fetching: false } as SessionContext,
});
return false;
}
dispatch({
type: 'UPDATE_STATE',
payload: { cart, fetching: false } as SessionContext,
});
return true;
};
const updateCart = (action: CartAction) => {
dispatch({
type: 'UPDATE_STATE',
payload: { fetching: true } as SessionContext,
});
return updateCartApiCall(action)
.then(setCart);
};
const findInCart = (
productId: number,
variationId?: number,
variation?: {
attributeName: string;
attributeValue: string;
}[],
extraData?: string) => {
const items = state?.cart?.contents?.nodes as CartItem[];
if (!items) {
return undefined;
}
return items.find(cartItemSearch(productId, variationId, variation, extraData, true)) || undefined;
};
useEffect(() => {
if (state.fetching) {
return;
}
if (!state.customer || !state.cart) {
fetchSession();
}
}, []);
const store: SessionContext = {
...
updateCart,
findInCart,
};
return (
<Provider value={store}>{children}</Provider>
);
}
export const useSession = () => useContext(sessionContext);有同樣的變相相相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相 updateCart 和 findInCart 有回話了.
updateCart: 執行以所提供为基础的推車動作CartAction物件.findInCart: 使用cartItemSearch功能以檢查所提供項目是否在推車中。 如果被找到,相對的CartItem有回放。
第4部分:建立 CartOptions
在这一部分中,我們要實行 CartOptions, SimpleCartOptions并 VariableCartOptions 构成部分。
1. 建立 /server/CartOptions:
// 伺服器/ Cart 選項/ Cart 選項
import { PropsWithChildren } from 'react';
import {
Product,
ProductTypesEnum,
SimpleProduct,
} from '@/graphql';
import { cn } from '@/utils/ui';
import { SimpleCartOptions } from '@/client/SimpleCartOptions';
import { VariableCartOptions } from '@/client/VariableCartOptions';
function Container({ className, children }: PropsWithChildren<{ className?: string }>) {
return (
<div
className={cn(
className && className,
'fixed inset-x-0 mx-auto p-4',
'lg:relative lg:inset-auto lg:mx-0 lg:p-0',
'bg-white bottom-0 z-30 w-screen',
'lg:bg-inherit lg:bottom-auto lg:z-auto lg:w-auto',
)}
>
{children}
</div>
);
}
export interface CartOptionsProps {
product: Product;
className?: string;
}
export function CartOptions(props: CartOptionsProps) {
const { product, className } = props;
const { type } = product as unknown as SimpleProduct;
let Component: (props: CartOptionsProps) => JSX.Element|null = () => null;
if (type === ProductTypesEnum.SIMPLE) {
Component = SimpleCartOptions;
} else if (type === ProductTypesEnum.VARIABLE) {
Component = VariableCartOptions;
}
return (
<Container className={className}>
<Component product={product}/>
</Container>
);
}CartOptions 有相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 在走之前 SimpleCartOptions 和 VariableCartOptions有能讓產品推車放出微風的通風通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通通
2. 建立 /hooks/useCartMutations.ts:
// 钩子/用量
import { useEffect, useMemo, useState } from 'react';
import { useSession } from '@woographql/client/SessionProvider';
export interface CartMutationInput {
mutation?: 'add'|'update'|'remove';
quantity?: number;
all?: boolean;
variation?: {
attributeName: string;
attributeValue: string;
}[]
}
const useCartMutations = (
productId: number,
variationId?: number,
variation?: {
attributeName: string;
attributeValue: string;
}[],
extraData?: string,
) => {
const {
cart,
updateCart,
findInCart,
fetching,
} = useSession();
const [quantityFound, setQuantityInCart] = useState(
findInCart(productId, variationId, variation, extraData)?.quantity as number || 0,
);
const itemKey = useMemo(
() => findInCart(productId, variationId, variation, extraData)?.key,
[findInCart, productId, variationId, variation, extraData],
);
useEffect(() => {
setQuantityInCart(
findInCart(productId, variationId, variation, extraData)?.quantity || 0,
);
}, [findInCart, productId, variationId, variation, extraData, cart?.contents?.nodes]);
async function mutate<T extends CartMutationInput>(values: T) {
const {
quantity = 1,
all = false,
mutation = 'update',
} = values;
if (!cart) {
return;
}
if (!productId) {
throw new Error('No item provided.');
// 向哨兵出錯 。 IO.
}
switch (mutation) {
case 'remove': {
if (!quantityFound) {
throw new Error('Provided item not in cart');
}
if (!itemKey) {
throw new Error('Failed to find item in cart.');
}
updateCart({
mutation: 'remove',
keys: [itemKey],
all,
});
break;
}
case 'update':
if (!quantityFound) {
throw new Error('Failed to find item in cart.');
}
if (!itemKey) {
throw new Error('Failed to find item in cart.');
}
updateCart({
mutation: 'update',
items: [{ key: itemKey, quantity }]
});
break;
default:
updateCart({
mutation: 'add',
quantity,
productId,
variationId,
variation,
extraData,
});
break;
}
}
const store = {
fetching,
quantityFound,
mutate
};
return store;
};
export default useCartMutations;
有至少一分 productId 该 useCartMutations 有勾勾取檢查 cart 最接近 SessionProvider 有相匹配的推車項目 productId。要注意的是, 搜尋非常精确地增加了所有類型產品和項目元的支持。 如果相關項目有變化 就必須提供相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 variationId 和 variation 物件. 就把被子放入去取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取出出出出出出出出出出取取取取出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出 SimpleCartOptions 构成部分。
三. 建立 /client/SimpleCartOptions:
// 客戶端/ SimpleCart 选项/ SimpleCart 选项.tsx
import {
FormEvent,
useState,
useEffect,
} from 'react';
import { cn } from '@/utils/ui';
import { Product, StockStatusEnum } from '@/graphql';
import type { ProductWithPrice } from '@/client/ShopProvider';
import useCartMutations from '@/hooks/useCartMutations';
import { Input } from '@/ui/input';
import { Button } from '@/ui/button';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { useToast } from '@/ui/use-toast';
interface CartOptionsProps {
product: Product;
className?: string;
}
export function SimpleCartOptions(props: CartOptionsProps) {
const { toast } = useToast();
const [quantity, setQuantity] = useState(1);
const [executing, setExecuting] = useState<'add'|'update'|'remove'|null>(null);
const { product } = props;
const {
rawPrice,
databaseId,
soldIndividually,
stockStatus,
stockQuantity,
manageStock,
} = product as ProductWithPrice;
const { fetching, mutate, quantityFound } = useCartMutations(databaseId);
const outOfStock = stockStatus === StockStatusEnum.OUT_OF_STOCK;
const mutation = quantityFound ? 'update' : 'add';
let submitButtonText = quantityFound ? 'Update' : 'Add To Basket';
if (outOfStock) {
submitButtonText = 'Out of Stock';
}
const maxQuantity = manageStock ? stockQuantity as number : undefined;
const onAddOrUpdate = async (event: FormEvent) => {
event.preventDefault();
setExecuting(mutation);
await mutate({ mutation, quantity });
if (mutation === 'add') {
toast({
title: 'Added to cart',
description: `${quantity} × ${product.name}`,
});
} else {
toast({
title: 'Updated cart',
description: `${quantity} × ${product.name}`,
});
}
};
const onRemove = async() => {
setExecuting('remove');
await mutate({ mutation: 'remove' });
toast({
title: 'Removed from cart',
description: `${quantity} × ${product.name}`,
});
};
useEffect(() => {
if (!fetching) {
setExecuting(null);
}
}, [fetching]);
useEffect(() => {
if (quantityFound) {
setQuantity(quantityFound);
}
}, [quantityFound]);
return (
<form
onSubmit={onAddOrUpdate}
className="flex flex-wrap gap-x-2 gap-y-4 items-center"
>
{(!soldIndividually || outOfStock) && (
<Input
className="basis-1/2 shrink"
type="number"
min={1}
max={maxQuantity}
value={quantity}
disabled={fetching}
onChange={(event) => setQuantity(Number(event.target.value))}
/>
)}
<p className="basis-auto grow text-center font-serif text-lg">
{outOfStock && 'Out Of Stock'}
{(!soldIndividually || outOfStock) && `× $${rawPrice} = `}
{!outOfStock && (<strong>{`$${Number(rawPrice) * quantity}`}</strong>)}
</p>
<div className="basis-full md:basis-auto flex gap-x-2">
<Button
type="submit"
className={cn(
"basis-full md:basis-auto inline-flex gap-2"
)}
disabled={fetching || outOfStock}
>
{submitButtonText}
{fetching && executing !== 'remove' && <LoadingSpinner noText/>}
</Button>
{!!quantityFound && (
<Button
type="button"
className="basis-full md:basis-auto inline-flex gap-2 bg-red-500"
onClick={onRemove}
disabled={fetching}
>
Remove
{fetching && executing === 'remove' && <LoadingSpinner noText/>}
</Button>
)}
</div>
</form>
);
}有名有名地 useCartMutations 就一分一分 productId 就因為我剛想搞出一出活 我們也直接取出一些值 product 因此可以依項目的可提供性而使移動選取表。 有快事需要注意:
soldIndividually有一面布魯克旗表示要用一面去取出相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相相相關相關相相相相相相相相相相關相關相相相相相相關相相關相關相關相相相相相關相相相相相相關相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相true有活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活stockStatus:一查一查能保持出產的股票狀態. 可能值有:OUT_OF_STOCK,ON_BACKORDER并IN_STOCKmanageStock有一面布林克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克克stockQuantity: 數字值,以表示所剩產品的數量,如果manageStock是true有被忽略了.
如果最後一部份很複雜 我想先道個歉 但是如果能幫助你放鬆心智 就由相關相關相關相關相關相關相關部分 因此,恭喜你走得這麼遠并感謝你跟我站在一起.
有再多的拖延了 VariableCartOptions有活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活
4. 有 建立 /client/VariableCartOptions:
// 有相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相相通相通相通相通相通相通相通相通相相相通相通相通相相相通相通相通相通
import {
FormEvent,
useState,
useEffect,
CSSProperties,
} from 'react';
import { cn } from '@/utils/ui';
import {
Product,
VariableProduct,
StockStatusEnum,
GlobalProductAttribute,
TermNode,
VariationAttribute,
ProductVariation,
} from '@/graphql';
import useCartMutations from '@/hooks/useCartMutations';
import { useProductContext } from '@/client/ProductProvider';
import { Input } from '@/ui/input';
import { Button } from '@/ui/button';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { useToast } from '@/ui/use-toast';
import { RadioGroup, RadioGroupItem } from '@/ui/radio-group';
import { Label } from '@/ui/label';
function ucfirst(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
type SelectedProductAttributes = { [key: string]: string };
interface CartOptionsProps {
product: Product;
className?: string;
}
export function VariableCartOptions({ product }: CartOptionsProps) {
const { toast } = useToast();
const [quantity, setQuantity] = useState(1);
const [executing, setExecuting] = useState<'add'|'update'|'remove'|null>(null);
const { get, selectVariation, hasSelectedVariation, selectedVariation } = useProductContext();
const rawPrice = get('rawPrice' as keyof Product) as string;
const soldIndividually = get('soldIndividually') as boolean;
const stockStatus = get('stockStatus') as StockStatusEnum;
const stockQuantity = get('stockQuantity') as number;
const manageStock = get('manageStock') as boolean;
const attributes = product.attributes?.nodes || [];
const variations = ((product as VariableProduct).variations?.nodes || []) as ProductVariation[];
const defaultAttributes = (product as VariableProduct).defaultAttributes?.nodes || [];
const [selectedAttributes, selectAttributes] = useState<SelectedProductAttributes>(
(defaultAttributes || []).reduce(
(results, attribute) => {
const { value, label } = attribute as VariationAttribute;
return {
...results,
[label as string]: value as string,
};
},
{},
),
);
useEffect(() => {
const variation = variations && variations.find(
({ attributes: variationAttributes }) => (
variationAttributes?.nodes as VariationAttribute[] || []
)?.every(
({ value, label }) => {
const index = ucfirst((label as string).replace(/_|-/g, ' '));
return !value || selectedAttributes[index] === value;
}
),
);
selectVariation(variation);
}, [selectedAttributes, product]);
const productId = product.databaseId;
const variationId = hasSelectedVariation ? get('databaseId') as number : undefined;
// 在變更上不加入任何屬性 。
const variationAttributes = selectedVariation?.attributes?.nodes || [];
const variation = Object.entries((selectedAttributes))
.filter(([attributeName, attributeValue]) => {
return !!variationAttributes.find((variationAttribute) => {
const { value, label } = variationAttribute as VariationAttribute;
return !value && label === attributeName;
});
})
.map(([attributeName, attributeValue]) => ({ attributeName: attributeName.toLowerCase(), attributeValue }));
const { fetching, mutate, quantityFound } = useCartMutations(productId, variationId, variation);
const outOfStock = stockStatus === StockStatusEnum.OUT_OF_STOCK;
const mutation = quantityFound ? 'update' : 'add';
let submitButtonText = quantityFound ? 'Update' : 'Add To Basket';
if (outOfStock) {
submitButtonText = 'Out of Stock';
}
const maxQuantity = manageStock ? stockQuantity as number : undefined;
const onAddOrUpdate = async (event: FormEvent) => {
event.preventDefault();
setExecuting(mutation);
await mutate({ mutation, quantity });
if (mutation === 'add') {
toast({
title: 'Added to cart',
description: `${quantity} × ${product.name}`,
});
} else {
toast({
title: 'Updated cart',
description: `${quantity} × ${product.name}`,
});
}
};
const onRemove = async() => {
setExecuting('remove');
await mutate({ mutation: 'remove' });
toast({
title: 'Removed from cart',
description: `${quantity} × ${product.name}`,
});
};
useEffect(() => {
if (!fetching) {
setExecuting(null);
}
}, [fetching]);
useEffect(() => {
if (quantityFound) {
setQuantity(quantityFound);
} else {
setQuantity(1);
}
}, [quantityFound]);
return (
<form
onSubmit={onAddOrUpdate}
className="flex flex-wrap gap-x-2 gap-y-4 items-center"
>
<div className="w-full">
{(attributes || []).map((attribute) => {
const {
id,
name,
label,
options,
variation: isVariationAttribute,
terms,
} = attribute as GlobalProductAttribute;
if (!isVariationAttribute) {
return null;
}
return (
<div key={id} className="w-full flex gap-x-4">
<p className="text-lg font-serif font-medium">{label || name}</p>
<RadioGroup
className="flex gap-2"
name={name as string}
onValueChange={(value) => {
selectAttributes({
...selectedAttributes,
[name as string]: value,
});
}}
>
{(terms?.nodes || options)?.map((option) => {
let value: string;
let buttonLabel: string;
let style: CSSProperties|undefined;
let id;
if (typeof option !== 'object') {
id = `${name}-${option}`;
value = option as string;
buttonLabel = option
.replace('-', ' ')
.replace(/^\w/, (c) => c.toUpperCase());
} else {
const { id: globalId, name: termName, slug } = option as TermNode;
id = globalId;
value = termName as string;
buttonLabel = termName as string;
if (name?.toLowerCase() === 'color') {
style = {
backgroundColor: slug as string,
}
}
}
return (
<div key={id} className="flex items-center space-x-2">
<RadioGroupItem
id={id}
className="w-6 h-6 text-lg"
value={value}
checked={selectedAttributes[name as string] === value}
style={style}
/>
<Label htmlFor={id}>{buttonLabel}</Label>
</div>
);
})}
</RadioGroup>
</div>
);
})}
</div>
{(hasSelectedVariation) ? (
<>
{(!soldIndividually || outOfStock) && (
<Input
className="basis-1/2 shrink"
type="number"
min={1}
max={maxQuantity}
value={quantity}
disabled={fetching}
onChange={(event) => setQuantity(Number(event.target.value))}
/>
)}
<p className="basis-auto grow text-center font-serif text-lg">
{outOfStock && 'Out Of Stock'}
{(!soldIndividually || outOfStock) && `× $${rawPrice} = `}
{!outOfStock && (<strong>{`$${Number(rawPrice) * quantity}`}</strong>)}
</p>
<div className="basis-full md:basis-auto flex gap-x-2">
<Button
type="submit"
className={cn(
"basis-full md:basis-auto inline-flex gap-2"
)}
disabled={fetching || outOfStock}
>
{submitButtonText}
{fetching && executing !== 'remove' && <LoadingSpinner noText/>}
</Button>
{!!quantityFound && (
<Button
type="button"
className="basis-full md:basis-auto inline-flex gap-2 bg-red-500"
onClick={onRemove}
disabled={fetching}
>
Remove
{fetching && executing === 'remove' && <LoadingSpinner noText/>}
</Button>
)}
</div>
</>
) : (
<p className="basis-full md:basis-auto text-center font-serif text-lg">
This product is not available at this time. Sorry, for the inconvenience.
</p>
)}
</form>
);
}就去分解出一分一分 有同樣的活 SimpleCartOptions 相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 因此我將他們分開去教訓。
所以起先你可能會注意到 有同樣的價值被取出 product 在物件中 SimpleCartOptions有活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活
const rawPrice = get('rawPrice' as keyof Product) as string;
const soldIndividually = get('soldIndividually') as boolean;
const stockStatus = get('stockStatus') as StockStatusEnum;
const stockQuantity = get('stockQuantity') as number;
const manageStock = get('manageStock') as boolean;但是我們正在用取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取. get 从回呼叫 ProductProvider 因此所選取的變更會有同樣的值。
接著要強調的是相接器的分類 selectedAttributes 它出自于 product 物件 defaultAttributes如果沒有被設定出, 屬性收音機按鍵被放入上載中 。
const defaultAttributes = (product as VariableProduct).defaultAttributes?.nodes || [];
const [selectedAttributes, selectAttributes] = useState<SelectedProductAttributes>(
(defaultAttributes || []).reduce(
(results, attribute) => {
const { value, label } = attribute as VariationAttribute;
return {
...results,
[label as string]: value as string,
};
},
{},
),
);相當簡單,接著要想出相配的變更 selectedAttributes 已更新。
useEffect(() => {
const variation = variations && variations.find(
({ attributes: variationAttributes }) => (
variationAttributes?.nodes as VariationAttribute[] || []
)?.every(
({ value, label }) => {
const index = ucfirst((label as string).replace(/_|-/g, ' '));
return !value || selectedAttributes[index] === value;
}
),
);
selectVariation(variation);
}, [selectedAttributes, product]);如所見 useEffect 每次主機被啟動 product 物件或 selectedAttributes 有更新。 它能做的是相對相對 selectedAttributes 至 attributes 在可以找到相匹配的變相中。 如果: variation 有相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相相相關相相相相關相關相相相相相關相相關相相相相相相相相關相相相相相相相相相相相相相相相關相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 就這樣一來 就說出相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相當相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相
const productId = product.databaseId;
const variationId = hasSelectedVariation ? get('databaseId') as number : undefined;
// 在變更上不加入任何屬性 。
const variationAttributes = selectedVariation?.attributes?.nodes || [];
const variation = Object.entries((selectedAttributes))
.filter(([attributeName, attributeValue]) => {
return !!variationAttributes.find((variationAttribute) => {
const { value, label } = variationAttribute as VariationAttribute;
return !value && label === attributeName;
});
})
.map(([attributeName, attributeValue]) => ({ attributeName: attributeName.toLowerCase(), attributeValue }));
const { fetching, mutate, quantityFound } = useCartMutations(productId, variationId, variation);有出乎意料地出事了 有出事了 variation 有常. 它想得到 find 在 useEffect 从上一段取出,取出倒數取出 selectedAttributes 物件并查找在 selectedVariation 有地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地圖取出地地相去地地地地地地地取出地取出地取出地取出地取出地取出地取出地取出地取出地圖取出地取出地取出地取出地取出地取出地取出地取出地取出地取出地取出地取出地 useCartMutation有 variation 參數 。 此參數后被傳送至我們 findInCart 選取可辨識出推車的變更。
有了這個 就注意到了 在推車上加入變更后 Update 和 Remove 以收音機按鈕改變所選取的屬性后, Update 和 Remove 已消失, 因為新選取的變更不是推車 。 這是我們想要的行為
現在我們可以放入這個并叫它做 但我們做最後一件事去搞出出產品頁面 ProductImage 使用 get 从 SessionProvider 以變更顯示相片到相片 selectedVariation有活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活
5. 第 二 章 更新 /client/ProductImage:
// 第 二 章
...
import { useProductContext } from '@/client/ProductProvider';
...
export function ProductImage({ product }: ProductImageProps) {
const { get } = useProductContext();
const sourceUrl = get('image.sourceUrl' as keyof Product) as string || product?.image?.sourceUrl;
const altText = get('image.altText' as keyof Product) as string || product?.image?.altText || '';
...
}就這樣了. 當你改變了 ProductProvider 更改 selectedVariation 如果變更有影像集, 所顯示的影像會被變更 。
第 二 部 分
有了 就能活到我們相關教學了 在這些教程中, 你從地上搭建了一個強大而方便用戶的eCommerce商店,
三更之行
有了 就能活到我們相關教學了 在這些教程中, 你從地上搭建了一個強大而方便用戶的eCommerce商店,
- 从建立你的商店的基礎結構起, 你已學會了如何能动态取出和顯示出產品清單, 有一套活活活的活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活活
- 此外,您也學會了如何處理最终用户會話,能使由下一個.js應用程式無缝地轉接到由WordPress安裝所主辦的WooCommerce商店. 這項功能是一款遊戲變更器, 讓我們能用上本地的WooCommerce頁面,
- 有相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相關相關相關相相相相相相相關相關相相相相相相相相相相相關相相相相相相相相相相相相關相相相相相相相相相相相相相相相相相相相相相 這些地點已同
SessionProvider有平滑的購物經驗相接而出
記住 在這些教程中引入的所有概念、方法及代碼 都是個坚实的起點 根据你eCommerce商店的相關相關需要,
恭喜你的成就! 您已掌握了用相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相關相相關相關相相關相關相相相關相關相相關相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 繼續去學習 做實驗并提升你的應用性能 eCommerce的世界也相去相去不遠了 你的商店也相去不遠了 我們想看看您能出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出入出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出入出出出出出出出出出出出出出出出出出出出出出出出出入出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出

Leave a Reply