feat(catalog): connect product pages to backend
This commit is contained in:
+58
-89
@@ -1,96 +1,57 @@
|
||||
import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import './index.scss'
|
||||
import { CATEGORIES } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
|
||||
// 成品展示配图(从 img 里取对应实物图)
|
||||
const SHOWCASE_LIST = [
|
||||
{
|
||||
title: '毕业纪念笔记本',
|
||||
desc: '全班名字组成校徽',
|
||||
priceText: '¥45 起',
|
||||
categoryId: 'notebook-large',
|
||||
image: assetUrl('/img/book_small/The1.jpg')
|
||||
},
|
||||
{
|
||||
title: '铜质杯垫',
|
||||
desc: '金属质感桌面艺术',
|
||||
priceText: '¥25 起',
|
||||
categoryId: 'coaster',
|
||||
image: assetUrl('/img/cup/The1.jpg')
|
||||
},
|
||||
{
|
||||
title: '竹制笔盒',
|
||||
desc: '自然竹纹文房雅器',
|
||||
priceText: '¥75 起',
|
||||
categoryId: 'penbox',
|
||||
image: assetUrl('/img/penbox/The1.jpg')
|
||||
},
|
||||
{
|
||||
title: '书本型灯',
|
||||
desc: '温暖光影点亮心意',
|
||||
priceText: '¥45 起',
|
||||
categoryId: 'booklamp',
|
||||
image: assetUrl('/img/booklight/The1.jpg')
|
||||
},
|
||||
{
|
||||
title: '情侣定制礼',
|
||||
desc: '两个人的名字交织',
|
||||
priceText: '¥45 起',
|
||||
categoryId: 'notebook-large',
|
||||
image: assetUrl('/img/book_big/The1.jpg')
|
||||
},
|
||||
{
|
||||
title: '企业年会礼',
|
||||
desc: '员工名字组成Logo',
|
||||
priceText: '¥75 起',
|
||||
categoryId: 'penbox',
|
||||
image: assetUrl('/img/penbox/The2.jpg')
|
||||
}
|
||||
]
|
||||
|
||||
// 品类对应的实物照片映射(首页卡片顶部大图)
|
||||
const PRODUCT_IMG_MAP: Record<string, string> = {
|
||||
'notebook-small': assetUrl('/img/book_small/The1.jpg'),
|
||||
'notebook-large': assetUrl('/img/book_big/The1.jpg'),
|
||||
'coaster': assetUrl('/img/cup/The1.jpg'),
|
||||
'penbox': assetUrl('/img/penbox/The1.jpg'),
|
||||
'booklamp': assetUrl('/img/booklight/The1.jpg')
|
||||
}
|
||||
import type { ProductCategory } from '../../types'
|
||||
import { fetchProducts } from '../../utils/api/product'
|
||||
import { toProductCategory } from '../../utils/productAdapter'
|
||||
|
||||
export default function Index() {
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
const safe = useSafeArea()
|
||||
useStatusBar(resolvedTheme)
|
||||
const [searchKey, setSearchKey] = useState('')
|
||||
const [products, setProducts] = useState<ProductCategory[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const loadProducts = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const result = await fetchProducts({ page: 1, pageSize: 100 })
|
||||
setProducts(result.list.map(toProductCategory))
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '商品加载失败,请重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadProducts()
|
||||
}, [loadProducts])
|
||||
|
||||
const navigateToProduct = (categoryId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/product/index?id=${categoryId}` })
|
||||
}
|
||||
|
||||
const navigateFromShowcase = (item: { categoryId?: string; title: string }) => {
|
||||
if (item.categoryId) {
|
||||
navigateToProduct(item.categoryId)
|
||||
return
|
||||
}
|
||||
Taro.showToast({
|
||||
title: `“${item.title}”定制通道即将开放`,
|
||||
icon: 'none'
|
||||
})
|
||||
const navigateFromShowcase = (item: ProductCategory) => {
|
||||
navigateToProduct(item.id)
|
||||
}
|
||||
|
||||
const filteredCategories = searchKey.trim()
|
||||
? CATEGORIES.filter(
|
||||
? products.filter(
|
||||
(c) => c.name.includes(searchKey) || c.desc.includes(searchKey)
|
||||
)
|
||||
: CATEGORIES
|
||||
: products
|
||||
const showcaseProducts = products.slice(0, 5)
|
||||
|
||||
const handleSearch = (e: any) => {
|
||||
setSearchKey(e.detail.value)
|
||||
@@ -133,26 +94,32 @@ export default function Index() {
|
||||
{/* 定制成品展示 */}
|
||||
<View className='showcase-section'>
|
||||
<Text className='home-section-title'>定制成品展示</Text>
|
||||
<Swiper
|
||||
className='showcase-swiper'
|
||||
circular
|
||||
autoplay
|
||||
interval={3000}
|
||||
duration={500}
|
||||
indicatorDots
|
||||
>
|
||||
{SHOWCASE_LIST.map((item, idx) => (
|
||||
<SwiperItem key={idx} className='showcase-swiper-item'>
|
||||
{showcaseProducts.length > 0 ? (
|
||||
<Swiper
|
||||
className='showcase-swiper'
|
||||
circular
|
||||
autoplay
|
||||
interval={3000}
|
||||
duration={500}
|
||||
indicatorDots
|
||||
>
|
||||
{showcaseProducts.map(item => (
|
||||
<SwiperItem key={item.id} className='showcase-swiper-item'>
|
||||
<View className='hero-block'>
|
||||
<Image className='hero-media' src={item.image} mode='aspectFill' />
|
||||
<Image className='hero-media' src={item.images[0] || item.iconImg || assetUrl('/icon/四角星.svg')} mode='aspectFill' />
|
||||
<View className='glass-overlay'>
|
||||
<Text className='hero-title'>{item.title}</Text>
|
||||
<Text className='hero-title'>{item.name}</Text>
|
||||
<Text className='hero-desc'>{item.desc}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
</Swiper>
|
||||
) : (
|
||||
<View className='empty-category'>
|
||||
<Text className='empty-text'>{loading ? '正在加载商品…' : error || '暂无商品'}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 热门品类 */}
|
||||
@@ -168,7 +135,7 @@ export default function Index() {
|
||||
<View className='media-card-image'>
|
||||
<Image
|
||||
className='media-card-img'
|
||||
src={PRODUCT_IMG_MAP[cat.id] || assetUrl('/icon/四角星.svg')}
|
||||
src={cat.images[0] || cat.iconImg || assetUrl('/icon/四角星.svg')}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
</View>
|
||||
@@ -183,8 +150,10 @@ export default function Index() {
|
||||
{filteredCategories.length === 0 && (
|
||||
<View className='empty-category'>
|
||||
<Image className='empty-icon-img' src={assetUrl('/icon/搜索.svg')} mode='aspectFit' />
|
||||
<Text className='empty-text'>未找到匹配的品类</Text>
|
||||
<Text className='empty-sub'>尝试搜索“笔记本”、“杯垫”等</Text>
|
||||
<Text className='empty-text'>{error || (loading ? '正在加载商品…' : '未找到匹配的品类')}</Text>
|
||||
<Text className='empty-sub' onTap={error ? loadProducts : undefined}>
|
||||
{error ? '点击重新加载' : '尝试搜索“笔记本”、“杯垫”等'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -193,19 +162,19 @@ export default function Index() {
|
||||
<View className='recommend-section'>
|
||||
<Text className='home-section-title'>为你推荐</Text>
|
||||
<View className='home-grid'>
|
||||
{SHOWCASE_LIST.slice(0, 4).map((item, idx) => (
|
||||
{showcaseProducts.slice(0, 4).map(item => (
|
||||
<View
|
||||
key={idx}
|
||||
key={item.id}
|
||||
className='media-card'
|
||||
onTap={() => navigateFromShowcase(item)}
|
||||
>
|
||||
<View className='media-card-image'>
|
||||
<Image className='media-card-img' src={item.image} mode='aspectFill' />
|
||||
<Image className='media-card-img' src={item.images[0] || item.iconImg || assetUrl('/icon/四角星.svg')} mode='aspectFill' />
|
||||
</View>
|
||||
<View className='media-card-body'>
|
||||
<Text className='media-card-title'>{item.title}</Text>
|
||||
<Text className='media-card-title'>{item.name}</Text>
|
||||
<Text className='media-card-desc'>{item.desc}</Text>
|
||||
<Text className='media-card-price'>{item.priceText}</Text>
|
||||
<Text className='media-card-price'>¥{item.price} 起</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import './index.scss'
|
||||
import { getProductById } from '../../utils/productConfig'
|
||||
import { addDesign, getDesignList, setDesignList } from '../../utils/store'
|
||||
import { createDesign } from '../../utils/api'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
@@ -12,18 +11,53 @@ import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../components/BottomActionBar'
|
||||
import { getProductIconImg } from '../../utils/productConfig'
|
||||
import type { ProductCategory } from '../../types'
|
||||
import { fetchProduct } from '../../utils/api/product'
|
||||
import { toProductCategory } from '../../utils/productAdapter'
|
||||
|
||||
export default function ProductPage() {
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
const { resolvedTheme } = useThemeContext()
|
||||
const safe = useSafeArea()
|
||||
useStatusBar(resolvedTheme)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [product, setProduct] = useState<ProductCategory | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const router = Taro.getCurrentInstance().router
|
||||
const params = router ? router.params : undefined
|
||||
const productId = params && params.id ? params.id : ''
|
||||
const product = getProductById(productId)
|
||||
|
||||
const loadProduct = useCallback(async () => {
|
||||
if (!productId) {
|
||||
setError('缺少商品 ID')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
setProduct(toProductCategory(await fetchProduct(productId)))
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '商品加载失败,请重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [productId])
|
||||
|
||||
useEffect(() => {
|
||||
loadProduct()
|
||||
}, [loadProduct])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<View className='product-page'><Text>正在加载商品…</Text></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
@@ -41,7 +75,7 @@ export default function ProductPage() {
|
||||
<Text className='back-arrow'>←</Text>
|
||||
</View>
|
||||
<View className='nav-placeholder' />
|
||||
<Text className='nav-title'>商品未找到</Text>
|
||||
<Text className='nav-title'>{error || '商品未找到'}</Text>
|
||||
<View className='nav-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,8 +2,7 @@ import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import './index.scss'
|
||||
import { PRODUCTS } from '../../../utils/productConfig'
|
||||
import { ProductCategory } from '../../../types'
|
||||
import { fetchProduct, type ProductDto } from '../../../utils/api/product'
|
||||
import { useSafeArea } from '../../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../../hooks/useStatusBar'
|
||||
import { useThemeContext } from '../../../context/ThemeContext'
|
||||
@@ -19,6 +18,9 @@ export default function ShopDetailPage() {
|
||||
const { resolvedTheme } = useThemeContext()
|
||||
const [winHeight, setWinHeight] = useState(667)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [product, setProduct] = useState<ProductDto | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const safe = useSafeArea()
|
||||
// 顶部为沉浸式商品图,状态栏始终使用白字。
|
||||
useStatusBar(resolvedTheme, { mode: 'light' })
|
||||
@@ -26,7 +28,27 @@ export default function ShopDetailPage() {
|
||||
const routerParams = Taro.getCurrentInstance().router
|
||||
const routerParamsValues = routerParams ? routerParams.params : undefined
|
||||
const productId = routerParamsValues ? routerParamsValues.id : ''
|
||||
const product: ProductCategory | undefined = PRODUCTS.find(p => p.id === productId)
|
||||
|
||||
const loadProduct = useCallback(async () => {
|
||||
if (!productId) {
|
||||
setError('缺少商品 ID')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
setProduct(await fetchProduct(productId))
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '商品加载失败,请重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [productId])
|
||||
|
||||
useEffect(() => {
|
||||
loadProduct()
|
||||
}, [loadProduct])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -108,12 +130,23 @@ export default function ShopDetailPage() {
|
||||
|
||||
const tone = useMemo(() => (product ? product.tone : [28, 22, 18]), [product])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="shop-detail-page theme-dark">
|
||||
<ThemedPageMeta darkTop />
|
||||
<Text style={{ color: '#fff', padding: '40rpx' }}>正在加载商品…</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<View className="shop-detail-page theme-dark">
|
||||
<ThemedPageMeta darkTop />
|
||||
<ScrollTopMask title="商品详情" targetSelector=".shop-back-btn" showBack />
|
||||
<Text style={{ color: '#fff', padding: '40rpx' }}>商品不存在</Text>
|
||||
<View style={{ padding: '40rpx' }} onTap={loadProduct}>
|
||||
<Text style={{ color: '#fff' }}>{error || '商品不存在'},点击重试</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,6 +95,18 @@
|
||||
font-variant: tabular-nums;
|
||||
}
|
||||
|
||||
.shop-feedback {
|
||||
display: block;
|
||||
padding: 48rpx 24rpx;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.shop-feedback-action {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Responsive: container center on wide screens
|
||||
============================================================ */
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import './index.scss'
|
||||
import { PRODUCTS } from '../../utils/productConfig'
|
||||
import { fetchProducts, type ProductDto } from '../../utils/api/product'
|
||||
|
||||
export default function ShopPage() {
|
||||
const { resolvedTheme } = useThemeContext()
|
||||
const safe = useSafeArea()
|
||||
useStatusBar(resolvedTheme)
|
||||
const [products, setProducts] = useState<ProductDto[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const loadProducts = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const result = await fetchProducts({ page: 1, pageSize: 100 })
|
||||
setProducts(result.list)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '商品加载失败,请重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadProducts()
|
||||
}, [loadProducts])
|
||||
|
||||
const openDetail = (productId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/shop/detail/index?id=${productId}` })
|
||||
@@ -28,7 +49,7 @@ export default function ShopPage() {
|
||||
</View>
|
||||
|
||||
<View className="shop-grid">
|
||||
{PRODUCTS.map(product => {
|
||||
{products.map(product => {
|
||||
const imageSrc = product.images[0] || product.iconImg || ''
|
||||
return (
|
||||
<View
|
||||
@@ -46,7 +67,7 @@ export default function ShopPage() {
|
||||
</View>
|
||||
<View className="shop-card-body">
|
||||
<Text className="shop-card-name">{product.name}</Text>
|
||||
<Text className="shop-card-desc">{product.desc || ''}</Text>
|
||||
<Text className="shop-card-desc">{product.subtitle || product.description || ''}</Text>
|
||||
<Text className="shop-card-price">¥{product.price}</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -54,6 +75,16 @@ export default function ShopPage() {
|
||||
})}
|
||||
</View>
|
||||
|
||||
{loading && <Text className="shop-feedback">正在加载商品…</Text>}
|
||||
{!loading && error && (
|
||||
<View className="shop-feedback shop-feedback-action" onTap={loadProducts}>
|
||||
<Text>加载失败:{error}。点击重试</Text>
|
||||
</View>
|
||||
)}
|
||||
{!loading && !error && products.length === 0 && (
|
||||
<Text className="shop-feedback">暂无在售商品</Text>
|
||||
)}
|
||||
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -44,6 +44,10 @@ export interface ProductCategory {
|
||||
specs?: [string, string][] // 规格参数键值对
|
||||
mask: MaskConfig
|
||||
images: string[] // 商品实物照片路径列表(相对页面引用的路径)
|
||||
/** 服务端商品目录字段;本地兜底数据可不填。 */
|
||||
status?: 'ON_SALE'
|
||||
sort?: number
|
||||
categoryId?: string
|
||||
}
|
||||
|
||||
/** 贴纸物品 */
|
||||
|
||||
@@ -1,16 +1,83 @@
|
||||
import http from '../request'
|
||||
import type { MaskConfig } from '../../types'
|
||||
|
||||
/** 商品列表 */
|
||||
export async function fetchProducts() {
|
||||
return http.get('/api/products', { auth: false })
|
||||
export interface ProductDto {
|
||||
id: string
|
||||
name: string
|
||||
categoryId?: string
|
||||
price: number
|
||||
originalPrice?: number
|
||||
leadTime: string
|
||||
subtitle?: string
|
||||
description?: string
|
||||
story?: string
|
||||
scene?: string
|
||||
tags: string[]
|
||||
specs: [string, string][]
|
||||
tone: [number, number, number]
|
||||
mask: MaskConfig
|
||||
images: string[]
|
||||
iconImg?: string
|
||||
status: 'ON_SALE'
|
||||
sort: number
|
||||
}
|
||||
|
||||
/** 商品详情 */
|
||||
export async function fetchProduct(id: string) {
|
||||
return http.get(`/api/products/${id}`, { auth: false })
|
||||
export interface CategoryDto {
|
||||
id: string
|
||||
name: string
|
||||
parentId?: string | null
|
||||
sort: number
|
||||
}
|
||||
|
||||
/** 分类列表 */
|
||||
export async function fetchCategories() {
|
||||
return http.get('/api/categories', { auth: false })
|
||||
export interface ProductListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
categoryId?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export interface ProductListResult {
|
||||
list: ProductDto[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
function toNumber(value: number | string | undefined): number | undefined {
|
||||
if (value === undefined) return undefined
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : undefined
|
||||
}
|
||||
|
||||
function normalizeProduct(product: ProductDto): ProductDto {
|
||||
const originalPrice = toNumber(product.originalPrice)
|
||||
return {
|
||||
...product,
|
||||
price: toNumber(product.price) ?? 0,
|
||||
...(originalPrice !== undefined ? { originalPrice } : {}),
|
||||
tags: product.tags || [],
|
||||
specs: product.specs || [],
|
||||
tone: product.tone || [28, 22, 18],
|
||||
images: product.images || [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 在售商品分页列表;公共接口,不携带登录 token。 */
|
||||
export async function fetchProducts(params: ProductListParams = {}): Promise<ProductListResult> {
|
||||
const query = Object.entries(params)
|
||||
.filter(([, value]) => value !== undefined && value !== '')
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
.join('&')
|
||||
const result = await http.get<ProductListResult>(`/api/products${query ? `?${query}` : ''}`, { auth: false })
|
||||
return { ...result, list: (result.list || []).map(normalizeProduct) }
|
||||
}
|
||||
|
||||
/** 单个在售商品;不存在或下架时后端返回 404。 */
|
||||
export async function fetchProduct(id: string): Promise<ProductDto> {
|
||||
const product = await http.get<ProductDto>(`/api/products/${encodeURIComponent(id)}`, { auth: false })
|
||||
return normalizeProduct(product)
|
||||
}
|
||||
|
||||
export async function fetchCategories(): Promise<CategoryDto[]> {
|
||||
return http.get<CategoryDto[]>('/api/categories', { auth: false })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ProductCategory } from '../types'
|
||||
import type { ProductDto } from './api/product'
|
||||
|
||||
/**
|
||||
* 将商品目录接口数据适配为现有 DIY/设计清单仍在使用的商品模型。
|
||||
* 页面只从接口读取商品;本适配层避免把后端字段差异散落到各个页面。
|
||||
*/
|
||||
export function toProductCategory(product: ProductDto): ProductCategory {
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
desc: product.subtitle || product.description || '',
|
||||
icon: product.iconImg || '',
|
||||
iconImg: product.iconImg,
|
||||
price: product.price,
|
||||
originalPrice: product.originalPrice,
|
||||
leadTime: product.leadTime,
|
||||
description: product.description || product.subtitle || '',
|
||||
subtitle: product.subtitle,
|
||||
tone: product.tone,
|
||||
story: product.story,
|
||||
scene: product.scene,
|
||||
tags: product.tags,
|
||||
specs: product.specs,
|
||||
mask: product.mask,
|
||||
images: product.images,
|
||||
status: product.status,
|
||||
sort: product.sort,
|
||||
categoryId: product.categoryId,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
declare const __API_BASE_URL__: string | undefined
|
||||
|
||||
/**
|
||||
* 后端接口请求封装
|
||||
* 后端统一响应:{ code, message, data },code === 0 表示成功
|
||||
* token 通过 Authorization: Bearer <token> 传入
|
||||
*/
|
||||
export const BASE_URL = 'https://wxbackend.tokenleaping.com'
|
||||
/** 由 Taro 构建时从 .env 注入;本机联调可设为 http://127.0.0.1:3090。 */
|
||||
export const BASE_URL = typeof __API_BASE_URL__ !== 'undefined'
|
||||
? __API_BASE_URL__
|
||||
: 'https://wxbackend.tokenleaping.com'
|
||||
|
||||
const TOKEN_KEY = 'smart_access_token'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user