发布商品,商品图片使用oss服务器地址
This commit is contained in:
@@ -13,27 +13,50 @@ type Product = {
|
||||
subtitle: string;
|
||||
price: number;
|
||||
benefitDisplay: number;
|
||||
mainImageUrl: string;
|
||||
mainImageUrl?: string | null;
|
||||
carouselUrls?: string[] | null;
|
||||
detailImageUrls?: string[] | null;
|
||||
aromaType: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type City = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const AROMA_TABS = [
|
||||
{ key: 'QINGXIANG', label: '清香型', open: true },
|
||||
{ key: 'JIANGXIANG', label: '酱香型', open: false },
|
||||
{ key: 'NONGXIANG', label: '浓香型', open: false },
|
||||
];
|
||||
|
||||
const CITY_STORAGE_KEY = 'dukang_selected_city';
|
||||
|
||||
export default function HomePage() {
|
||||
const [tab, setTab] = useState('QINGXIANG');
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [cities, setCities] = useState<City[]>([]);
|
||||
const [cityCode, setCityCode] = useState(() => localStorage.getItem(CITY_STORAGE_KEY) || '410100');
|
||||
const [toast, setToast] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
request<Product[]>('USER_H5', '/catalog/products').then(setProducts);
|
||||
request<City[]>('USER_H5', '/catalog/cities').then((list) => {
|
||||
setCities(list);
|
||||
if (!list.some((c) => c.code === cityCode) && list[0]) {
|
||||
setCityCode(list[0].code);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cityCode) return;
|
||||
localStorage.setItem(CITY_STORAGE_KEY, cityCode);
|
||||
request<Product[]>('USER_H5', `/catalog/products?cityCode=${encodeURIComponent(cityCode)}`).then(setProducts);
|
||||
}, [cityCode]);
|
||||
|
||||
function showToast(message: string) {
|
||||
setToast(message);
|
||||
window.setTimeout(() => setToast(''), 2200);
|
||||
@@ -47,6 +70,7 @@ export default function HomePage() {
|
||||
setTab(key);
|
||||
}
|
||||
|
||||
const selectedCity = cities.find((c) => c.code === cityCode);
|
||||
const filtered = products.filter((p) => p.aromaType === tab);
|
||||
const onSale = tab === 'QINGXIANG';
|
||||
|
||||
@@ -57,7 +81,16 @@ export default function HomePage() {
|
||||
extra={(
|
||||
<div className="tab-main-city">
|
||||
<span className="material-symbols-outlined">location_on</span>
|
||||
<span>郑州市</span>
|
||||
<select
|
||||
value={cityCode}
|
||||
onChange={(e) => setCityCode(e.target.value)}
|
||||
style={{ border: 'none', background: 'transparent', font: 'inherit', color: 'inherit' }}
|
||||
>
|
||||
{cities.map((c) => (
|
||||
<option key={c.code} value={c.code}>{c.name}</option>
|
||||
))}
|
||||
{!cities.length && <option value={cityCode}>{selectedCity?.name ?? '郑州市'}</option>}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -78,10 +111,10 @@ export default function HomePage() {
|
||||
<section className="home-product-list">
|
||||
{!onSale && <div className="home-empty">该香型暂未上线,敬请期待</div>}
|
||||
{onSale &&
|
||||
filtered.map((p, index) => (
|
||||
filtered.map((p) => (
|
||||
<article key={p.id} className="home-product-card">
|
||||
<Link to={`/product/${p.id}`} className="home-product-link">
|
||||
<ProductCarousel images={getProductImages(index)} alt={p.name} />
|
||||
<ProductCarousel images={getProductImages(p)} alt={p.name} />
|
||||
<div className="home-product-body">
|
||||
<div className="home-product-row">
|
||||
<h3 className="home-product-name">{p.name}</h3>
|
||||
|
||||
@@ -4,8 +4,6 @@ import SubPageHeader from '../components/SubPageHeader';
|
||||
import AppImage from '@dukang/shared-ui/AppImage';
|
||||
import { request } from '../lib/api';
|
||||
import { buildProductDetailUrl } from '../lib/navigation';
|
||||
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
|
||||
import { tryGetClientGpsLocation } from '../lib/client-location';
|
||||
import { getProductMainImage } from '../lib/product-images';
|
||||
import PhoneVerifySheet from '../components/PhoneVerifySheet';
|
||||
import { useUserSession } from '../contexts/UserSessionContext';
|
||||
@@ -27,6 +25,8 @@ type PreviewProduct = {
|
||||
spec: string;
|
||||
subtitle?: string;
|
||||
price: number;
|
||||
mainImageUrl?: string | null;
|
||||
carouselUrls?: string[] | null;
|
||||
};
|
||||
|
||||
type OrderPreview = {
|
||||
@@ -113,9 +113,7 @@ export default function OrderConfirmPage() {
|
||||
|
||||
const isCross = forceCross || preview?.deliveryType === 'CROSS_CITY';
|
||||
const minQty = isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2);
|
||||
const productIndex = productId ? Math.max(0, Number(productId) - 1) : 0;
|
||||
const productImage =
|
||||
productIndex === 0 ? STITCH_ORDER_PRODUCT_IMAGE : getProductMainImage(productIndex);
|
||||
const productImage = preview?.product ? getProductMainImage(preview.product) : getProductMainImage();
|
||||
|
||||
async function doSubmit() {
|
||||
const clientLocation = await tryGetClientGpsLocation();
|
||||
|
||||
@@ -145,6 +145,7 @@ export default function OrderDetailPage() {
|
||||
const productImage = item?.productImage || STITCH_ORDER_PRODUCT_IMAGE;
|
||||
const canEditAddress = order ? EDITABLE_STATUSES.has(order.status) : false;
|
||||
const canConfirmReceive = order?.status === 'PENDING_RECEIVE' && !isReship;
|
||||
const canRefund = order && ['PENDING_SHIP', 'PENDING_RECEIVE', 'COMPLETED', 'OUT_WAREHOUSE', 'SHIPPING'].includes(order.status);
|
||||
const productTotal = Number(order?.productAmount ?? order?.payAmount ?? 0);
|
||||
const freightTotal = Number(order?.freightAmount ?? 0);
|
||||
|
||||
@@ -174,6 +175,22 @@ export default function OrderDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestRefund() {
|
||||
if (!id || !canRefund) return;
|
||||
setConfirming(true);
|
||||
try {
|
||||
await request('USER_H5', `/trade/orders/${id}/refund-requests`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ remark: '用户申请退款' }),
|
||||
});
|
||||
await loadOrder();
|
||||
} catch (e) {
|
||||
window.alert(e instanceof Error ? e.message : '申请退款失败');
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!order) return <div className="empty">加载中...</div>;
|
||||
|
||||
return (
|
||||
@@ -380,6 +397,11 @@ export default function OrderDetailPage() {
|
||||
<span className="material-symbols-outlined">headset_mic</span>
|
||||
联系客服
|
||||
</button>
|
||||
{canRefund && order?.status !== 'REFUNDING' && order?.status !== 'REFUNDED' && (
|
||||
<button type="button" className="order-detail-action-outline" disabled={confirming} onClick={requestRefund}>
|
||||
申请退款
|
||||
</button>
|
||||
)}
|
||||
{canConfirmReceive && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -4,8 +4,9 @@ import AppImage from '@dukang/shared-ui/AppImage';
|
||||
import ProductCarousel from '../components/ProductCarousel';
|
||||
import { request } from '../lib/api';
|
||||
import { getProductCarouselImages, getProductDetailImages } from '../lib/product-images';
|
||||
import type { ProductImageSource } from '../lib/product-images';
|
||||
|
||||
type Product = {
|
||||
type Product = ProductImageSource & {
|
||||
name: string;
|
||||
subtitle?: string;
|
||||
price: number;
|
||||
@@ -22,7 +23,6 @@ export default function ProductDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [headerSolid, setHeaderSolid] = useState(false);
|
||||
const imageIndex = id ? Math.max(0, Number(id) - 1) : 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (id) request<Product>('USER_H5', `/catalog/products/${id}`).then(setProduct);
|
||||
@@ -39,8 +39,8 @@ export default function ProductDetailPage() {
|
||||
if (!product) return <div className="empty">加载中...</div>;
|
||||
|
||||
const benefit = Number(product.benefitAmount ?? product.price);
|
||||
const carouselImages = getProductCarouselImages(imageIndex);
|
||||
const detailImages = getProductDetailImages(imageIndex);
|
||||
const carouselImages = getProductCarouselImages(product);
|
||||
const detailImages = getProductDetailImages(product);
|
||||
|
||||
return (
|
||||
<div className="product-detail-page">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
|
||||
@@ -5,6 +6,19 @@ export default function RedeemCodePage() {
|
||||
const navigate = useNavigate();
|
||||
const token = sessionStorage.getItem('redeemToken') || '';
|
||||
const amount = sessionStorage.getItem('redeemAmount') || '0';
|
||||
const [secondsLeft, setSecondsLeft] = useState(300);
|
||||
|
||||
useEffect(() => {
|
||||
const expireAt = sessionStorage.getItem('redeemExpireAt');
|
||||
if (!expireAt) return;
|
||||
const tick = () => {
|
||||
const left = Math.max(0, Math.floor((new Date(expireAt).getTime() - Date.now()) / 1000));
|
||||
setSecondsLeft(left);
|
||||
};
|
||||
tick();
|
||||
const id = window.setInterval(tick, 1000);
|
||||
return () => window.clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page-no-tab" style={{ textAlign: 'center' }}>
|
||||
@@ -14,10 +28,14 @@ export default function RedeemCodePage() {
|
||||
<div className="label-md text-muted">核销金额</div>
|
||||
<div className="amount-xl" style={{ margin: '8px 0 24px' }}>¥{amount}</div>
|
||||
<div className="code-box">{token}</div>
|
||||
<p className="label-md text-muted" style={{ marginTop: 16 }}>5 分钟内有效</p>
|
||||
<p className="label-md text-muted" style={{ marginTop: 16 }}>
|
||||
{secondsLeft > 0 ? `${secondsLeft} 秒后过期` : '已过期,请重新生成'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="page-actions">
|
||||
<button type="button" className="btn btn-outline btn-block" onClick={() => navigate('/redeem/success')}>模拟核销完成</button>
|
||||
<button type="button" className="btn btn-outline btn-block" onClick={() => navigate('/benefit')}>
|
||||
返回权益页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user