diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..ad3e811 Binary files /dev/null and b/dist.zip differ diff --git a/src/apis/mall-app/address.ts b/src/apis/mall-app/address.ts new file mode 100644 index 0000000..1e0e322 --- /dev/null +++ b/src/apis/mall-app/address.ts @@ -0,0 +1,64 @@ +import { + APP_API_PATHS, + normalizeAppAddressDefaultSetResponse, + normalizeAppAddressListResponse, + normalizeAppAddressSaveResponse, + normalizeMallAppPayload, + postMallApp, +} from './shared' +import type { + AppAddressDefaultSetRequest, + AppAddressDefaultSetResponse, + AppAddressDeleteRequest, + AppAddressDeleteResponse, + AppAddressListRequest, + AppAddressListResponse, + AppAddressSaveRequest, + AppAddressSaveResponse, +} from '@/types/mall' + +export const apiAppAddressList = async (payload: AppAddressListRequest, signal?: AbortSignal) => { + const response = await postMallApp( + APP_API_PATHS.addressList, + normalizeMallAppPayload(payload), + signal + ) + + return { + ...response, + data: normalizeAppAddressListResponse(response.data), + } +} + +export const apiAppAddressSave = async (payload: AppAddressSaveRequest, signal?: AbortSignal) => { + const response = await postMallApp( + APP_API_PATHS.addressSave, + normalizeMallAppPayload(payload), + signal + ) + + return { + ...response, + data: normalizeAppAddressSaveResponse(response.data), + } +} + +export const apiAppAddressDelete = (payload: AppAddressDeleteRequest, signal?: AbortSignal) => + postMallApp( + APP_API_PATHS.addressDelete, + normalizeMallAppPayload(payload), + signal + ) + +export const apiAppAddressDefaultSet = async (payload: AppAddressDefaultSetRequest, signal?: AbortSignal) => { + const response = await postMallApp( + APP_API_PATHS.addressDefaultSet, + normalizeMallAppPayload(payload), + signal + ) + + return { + ...response, + data: normalizeAppAddressDefaultSetResponse(response.data), + } +} diff --git a/src/apis/mall-app/afterSale.ts b/src/apis/mall-app/afterSale.ts index 34c1608..db78320 100644 --- a/src/apis/mall-app/afterSale.ts +++ b/src/apis/mall-app/afterSale.ts @@ -1,15 +1,26 @@ -import { APP_API_PATHS, normalizeMallAppPayload, postMallApp } from './shared' +import { + APP_API_PATHS, + normalizeAppAfterSaleApplyResponse, + normalizeMallAppPayload, + postMallApp, +} from './shared' import type { AppAfterSaleApplyRequest, AppAfterSaleApplyResponse, } from '@/types/mall' -export const apiAppAfterSaleApply = ( +export const apiAppAfterSaleApply = async ( payload: AppAfterSaleApplyRequest, signal?: AbortSignal -) => - postMallApp( +) => { + const response = await postMallApp( APP_API_PATHS.afterSaleApply, normalizeMallAppPayload(payload), signal ) + + return { + ...response, + data: normalizeAppAfterSaleApplyResponse(response.data), + } +} diff --git a/src/apis/mall-app/cart.ts b/src/apis/mall-app/cart.ts index 6f401b4..9afcf25 100644 --- a/src/apis/mall-app/cart.ts +++ b/src/apis/mall-app/cart.ts @@ -1,4 +1,10 @@ -import { APP_API_PATHS, normalizeAppCartListResponse, normalizeMallAppPayload, postMallApp } from './shared' +import { + APP_API_PATHS, + normalizeAppCartListResponse, + normalizeAppCartSaveResponse, + normalizeMallAppPayload, + postMallApp, +} from './shared' import type { AppCartListRequest, AppCartListResponse, @@ -21,13 +27,19 @@ export const apiAppCartList = async (payload: AppCartListRequest, signal?: Abort } } -export const apiAppCartSave = (payload: AppCartSaveRequest, signal?: AbortSignal) => - postMallApp( +export const apiAppCartSave = async (payload: AppCartSaveRequest, signal?: AbortSignal) => { + const response = await postMallApp( APP_API_PATHS.cartSave, normalizeMallAppPayload(payload), signal ) + return { + ...response, + data: normalizeAppCartSaveResponse(response.data), + } +} + export const apiAppCartRemove = (payload: AppCartRemoveRequest, signal?: AbortSignal) => postMallApp( APP_API_PATHS.cartRemove, diff --git a/src/apis/mall-app/index.ts b/src/apis/mall-app/index.ts index a1dd192..ccd9db2 100644 --- a/src/apis/mall-app/index.ts +++ b/src/apis/mall-app/index.ts @@ -2,6 +2,7 @@ export * from './shared' export * from './home' export * from './goods' export * from './cart' +export * from './address' export * from './trade' export * from './order' export * from './afterSale' diff --git a/src/apis/mall-app/normalize.ts b/src/apis/mall-app/normalize.ts index c40a925..6b40b97 100644 --- a/src/apis/mall-app/normalize.ts +++ b/src/apis/mall-app/normalize.ts @@ -1,31 +1,43 @@ import type { + AppAddressDefaultSetResponse, + AppAddressListResponse, + AppAddressSaveResponse, + AppAfterSaleApplyResponse, AppCartListResponse, + AppCartSaveResponse, AppGoodsDetailResponse, AppGoodsPageResponse, AppHomeMerchantGoodsResponse, AppHomeResponse, AppRecentNewGoodsResponse, AppOrderDetailResponse, + AppOrderConfirmResponse, AppOrderPageResponse, + AppTradeCreateResponse, CartItemInfo, + CartGoodsGroupInfo, + CartMerchantGroupInfo, GoodsDetailInfo, GoodsSkuInfo, GoodsSummary, AppTradePreviewResponse, HomeMerchantGoodsBlock, + MerchantDeliveryOptionInfo, MerchantId, MerchantInfo, + MerchantPackageOptionInfo, OrderDetailInfo, OrderSummary, RecommendMerchantInfo, RecentNewGoodsItem, TradePreviewInfo, + TradePreviewItemInfo, TradePreviewStoreInfo, } from '@/types/mall' -type MaybeMallUidPayload = { +type MaybeMallIdentityPayload = { uid?: number - memberId?: number + memberId?: MerchantId | number } type MaybeMallMerchantPayload = { @@ -33,6 +45,22 @@ type MaybeMallMerchantPayload = { storeId?: number | string } +type MaybeMallIdPayload = { + id?: string | number + skuId?: string | number + skuIds?: Array + addressId?: string | number + orderId?: string | number + orderItemId?: string | number + categoryId?: string | number + buyItems?: Array<{ skuId?: string | number; buyNum?: number }> + merchantOptions?: Array<{ + merchantId?: MerchantId | number + deliveryOptionId?: string | number + packageOptionId?: string | number + }> +} + type RawRecommendMerchantPayload = Partial & { id?: RecommendMerchantInfo['id'] merchantId?: MerchantId | number | string @@ -56,6 +84,8 @@ type RawMerchantEntityPayload = Partial & { } type RawGoodsSummaryPayload = Partial & { + id?: GoodsSummary['id'] | number + spuId?: GoodsSummary['id'] | number merchantId?: MerchantId | number | string createdAt?: GoodsSummary['createdAt'] | string | null updatedAt?: GoodsSummary['updatedAt'] | string | null @@ -69,14 +99,29 @@ type RawGoodsSummaryPayload = Partial & { update_time?: number | string | null } -const toMerchantId = (value: MerchantId | number | string | null | undefined): MerchantId => - value == null ? '' : String(value) +const toId = (value: string | number | null | undefined): string => (value == null ? '' : String(value)) + +const toMerchantId = (value: MerchantId | number | string | null | undefined): MerchantId => toId(value) const toNumber = (value: number | string | null | undefined): number => { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : 0 } +const toBoolean = (value: boolean | number | string | null | undefined): boolean => { + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value !== 0 + } + if (typeof value === 'string') { + return ['1', 'true', 'yes'].includes(value.trim().toLowerCase()) + } + + return false +} + const pickText = (...values: Array): string => { for (const value of values) { if (typeof value === 'string' && value.trim()) { @@ -111,12 +156,52 @@ const toUnixTime = (value: number | string | null | undefined): number => { } export const normalizeMallAppPayload = (payload: TPayload): TPayload => { - const normalized = { ...payload } as TPayload & MaybeMallUidPayload & MaybeMallMerchantPayload + const normalized = { ...payload } as TPayload & + MaybeMallIdentityPayload & + MaybeMallMerchantPayload & + MaybeMallIdPayload - if (normalized.uid == null && normalized.memberId != null) { - normalized.uid = normalized.memberId + delete normalized.uid + if (normalized.memberId != null) { + normalized.memberId = toMerchantId(normalized.memberId) + } + + if (normalized.id != null) { + normalized.id = toId(normalized.id) + } + if (normalized.skuId != null) { + normalized.skuId = toId(normalized.skuId) + } + if (normalized.skuIds != null) { + normalized.skuIds = normalized.skuIds.map(toId) + } + if (normalized.addressId != null) { + normalized.addressId = toId(normalized.addressId) + } + if (normalized.orderId != null) { + normalized.orderId = toId(normalized.orderId) + } + if (normalized.orderItemId != null) { + normalized.orderItemId = toId(normalized.orderItemId) + } + if (normalized.categoryId != null) { + normalized.categoryId = toId(normalized.categoryId) + } + if (normalized.buyItems != null) { + normalized.buyItems = normalized.buyItems.map(item => ({ + ...item, + skuId: item.skuId == null ? item.skuId : toId(item.skuId), + })) + } + if (normalized.merchantOptions != null) { + normalized.merchantOptions = normalized.merchantOptions.map(option => ({ + ...option, + merchantId: option.merchantId == null ? option.merchantId : toMerchantId(option.merchantId), + deliveryOptionId: + option.deliveryOptionId == null ? option.deliveryOptionId : toId(option.deliveryOptionId), + packageOptionId: option.packageOptionId == null ? option.packageOptionId : toId(option.packageOptionId), + })) } - delete normalized.memberId if (normalized.merchantId != null) { normalized.merchantId = toMerchantId(normalized.merchantId) @@ -131,7 +216,10 @@ export const normalizeMallAppPayload = (payload: TPaylo const normalizeGoodsSku = (sku: GoodsSkuInfo): GoodsSkuInfo => ({ ...sku, + id: toId(sku.id), + spuId: String(sku.spuId), merchantId: toMerchantId(sku.merchantId), + storeId: toId(sku.storeId), }) const normalizeGoodsSummary = (goods: RawGoodsSummaryPayload): GoodsSummary => { @@ -139,9 +227,12 @@ const normalizeGoodsSummary = (goods: RawGoodsSummaryPayload): GoodsSummary => { return { ...normalized, + id: String(goods.spuId ?? goods.id ?? ''), merchantId: toMerchantId(goods.merchantId), merchantName: pickText(goods.merchantName), merchantLogo: pickText(goods.merchantLogo), + storeId: toId(goods.storeId), + categoryId: toId(goods.categoryId), createdAt: toUnixTime(goods.createdAt ?? goods.createAt ?? goods.created_at ?? goods.createTime ?? goods.create_time), updatedAt: toUnixTime(goods.updatedAt ?? goods.updateAt ?? goods.updated_at ?? goods.updateTime ?? goods.update_time), } @@ -159,7 +250,10 @@ const normalizeRecentNewGoodsItem = (goods: RecentNewGoodsItem): RecentNewGoodsI const normalizeGoodsDetailInfo = (detail: GoodsDetailInfo): GoodsDetailInfo => ({ ...detail, + id: toId(detail.id), merchantId: toMerchantId(detail.merchantId), + storeId: toId(detail.storeId), + categoryId: toId(detail.categoryId), createdAt: toUnixTime(detail.createdAt), updatedAt: toUnixTime(detail.updatedAt), skus: (detail.skus ?? []).map(normalizeGoodsSku), @@ -167,22 +261,104 @@ const normalizeGoodsDetailInfo = (detail: GoodsDetailInfo): GoodsDetailInfo => ( const normalizeCartItem = (item: CartItemInfo): CartItemInfo => ({ ...item, + id: toId(item.id), + memberId: item.memberId == null ? undefined : toMerchantId(item.memberId), merchantId: toMerchantId(item.merchantId), + merchantName: pickText(item.merchantName), + merchantLogo: pickText(item.merchantLogo), + storeId: toId(item.storeId), + spuId: String(item.spuId), + skuId: toId(item.skuId), + itemAmount: + item.itemAmount ?? + (Number.isFinite(Number(item.salePrice)) && Number.isFinite(Number(item.buyNum)) + ? (Number(item.salePrice) * Number(item.buyNum)).toFixed(2) + : '0'), +}) + +const normalizeCartGoodsGroup = (goods: CartGoodsGroupInfo): CartGoodsGroupInfo => ({ + ...goods, + spuId: String(goods.spuId), + spuName: pickText(goods.spuName), + coverUrl: pickText(goods.coverUrl), + checked: Boolean(goods.checked), + goodsAmount: goods.goodsAmount ?? '0', + checkedAmount: goods.checkedAmount ?? '0', + items: (goods.items ?? []).map(normalizeCartItem), +}) + +const normalizeCartMerchantGroup = (group: CartMerchantGroupInfo): CartMerchantGroupInfo => ({ + ...group, + merchantId: toMerchantId(group.merchantId), + merchantName: pickText(group.merchantName) || '未命名商家', + merchantLogo: pickText(group.merchantLogo), + checked: Boolean(group.checked), + itemCount: toNumber(group.itemCount), + goodsCount: toNumber(group.goodsCount), + goodsAmount: group.goodsAmount ?? '0', + checkedAmount: group.checkedAmount ?? '0', + goods: (group.goods ?? []).map(normalizeCartGoodsGroup), +}) + +const normalizeTradePreviewItem = (item: TradePreviewItemInfo): TradePreviewItemInfo => ({ + ...item, + spuId: String(item.spuId), + skuId: toId(item.skuId), +}) + +const normalizeDeliveryOption = (option: MerchantDeliveryOptionInfo): MerchantDeliveryOptionInfo => ({ + ...option, + id: toId(option.id), + merchantId: toMerchantId(option.merchantId), +}) + +const normalizePackageOption = (option: MerchantPackageOptionInfo): MerchantPackageOptionInfo => ({ + ...option, + id: toId(option.id), + merchantId: toMerchantId(option.merchantId), }) const normalizeTradePreviewGroup = (group: TradePreviewStoreInfo): TradePreviewStoreInfo => ({ ...group, merchantId: toMerchantId(group.merchantId), + storeId: group.storeId == null ? undefined : toId(group.storeId), + deliveryOptions: (group.deliveryOptions ?? []).map(normalizeDeliveryOption), + packageOptions: (group.packageOptions ?? []).map(normalizePackageOption), + selectedDeliveryOption: group.selectedDeliveryOption + ? normalizeDeliveryOption(group.selectedDeliveryOption) + : undefined, + selectedPackageOption: group.selectedPackageOption ? normalizePackageOption(group.selectedPackageOption) : undefined, + items: (group.items ?? []).map(normalizeTradePreviewItem), }) const normalizeOrderSummary = (order: OrderSummary): OrderSummary => ({ ...order, + id: toId(order.id), + tradeId: toId(order.tradeId), + memberId: order.memberId == null ? undefined : toMerchantId(order.memberId), merchantId: toMerchantId(order.merchantId), + storeId: toId(order.storeId), }) const normalizeOrderDetailInfo = (order: OrderDetailInfo): OrderDetailInfo => ({ ...order, + id: toId(order.id), + tradeId: toId(order.tradeId), + memberId: order.memberId == null ? undefined : toMerchantId(order.memberId), merchantId: toMerchantId(order.merchantId), + storeId: toId(order.storeId), + selectedDeliveryOption: order.selectedDeliveryOption + ? normalizeDeliveryOption(order.selectedDeliveryOption) + : undefined, + selectedPackageOption: order.selectedPackageOption ? normalizePackageOption(order.selectedPackageOption) : undefined, + items: (order.items ?? []).map(item => ({ + ...item, + id: toId(item.id), + orderId: toId(item.orderId), + tradeId: toId(item.tradeId), + spuId: String(item.spuId), + skuId: toId(item.skuId), + })), }) const normalizeRecommendMerchant = ( @@ -194,6 +370,7 @@ const normalizeRecommendMerchant = ( return { ...(merchant as RecommendMerchantInfo), + id: toId(merchant.id ?? merchant.merchantId), merchantId: toMerchantId(merchant.merchantId ?? merchant.id), storeName: pickText(merchant.storeName, merchant.merchantName), storeLogo: pickText(merchant.storeLogo, merchant.logo), @@ -215,6 +392,7 @@ const normalizeMerchantEntity = ( return { ...(merchant as MerchantInfo), + id: toId(merchant.id ?? merchant.merchantId), merchantId: toMerchantId(merchant.merchantId ?? merchant.id), storeName: pickText(merchant.storeName, merchant.merchantName), storeLogo: pickText(merchant.storeLogo, merchant.logo), @@ -223,6 +401,26 @@ const normalizeMerchantEntity = ( } } +const normalizeAddressInfo = (address: AppAddressListResponse['list'][number]) => ({ + ...address, + id: toId(address.id), +}) + +const normalizeBanner = (banner: AppHomeResponse['banners'][number]) => ({ + ...banner, + id: toId(banner.id), +}) + +const normalizeCategoryNav = (category: AppHomeResponse['categoryNavs'][number]) => ({ + ...category, + categoryId: toId(category.categoryId), +}) + +const normalizeChannel = (channel: AppHomeResponse['channels'][number]) => ({ + ...channel, + id: toId(channel.id), +}) + const normalizeHomeMerchantGoodsBlock = ( block: HomeMerchantGoodsBlock | undefined ): HomeMerchantGoodsBlock | undefined => { @@ -262,7 +460,40 @@ export const normalizeAppCartListResponse = ( payload: AppCartListResponse ): AppCartListResponse => ({ ...payload, - list: payload.list.map(normalizeCartItem), + merchantGroups: (payload.merchantGroups ?? []).map(normalizeCartMerchantGroup), + itemCount: toNumber(payload.itemCount), + goodsCount: toNumber(payload.goodsCount), + goodsAmount: payload.goodsAmount ?? '0', + checkedCount: toNumber(payload.checkedCount), + checkedAmount: payload.checkedAmount ?? '0', +}) + +export const normalizeAppCartSaveResponse = ( + payload: AppCartSaveResponse +): AppCartSaveResponse => ({ + ...payload, + cartId: toId(payload.cartId), +}) + +export const normalizeAppAddressListResponse = ( + payload: AppAddressListResponse +): AppAddressListResponse => ({ + ...payload, + list: (payload.list ?? []).map(normalizeAddressInfo), +}) + +export const normalizeAppAddressSaveResponse = ( + payload: AppAddressSaveResponse +): AppAddressSaveResponse => ({ + ...payload, + addressId: toId(payload.addressId), +}) + +export const normalizeAppAddressDefaultSetResponse = ( + payload: AppAddressDefaultSetResponse +): AppAddressDefaultSetResponse => ({ + ...payload, + addressId: toId(payload.addressId), }) export const normalizeAppOrderPageResponse = ( @@ -279,6 +510,20 @@ export const normalizeAppOrderDetailResponse = ( data: normalizeOrderDetailInfo(payload.data), }) +export const normalizeAppOrderConfirmResponse = ( + payload: AppOrderConfirmResponse +): AppOrderConfirmResponse => ({ + ...payload, + orderId: toId(payload.orderId), +}) + +export const normalizeAppAfterSaleApplyResponse = ( + payload: AppAfterSaleApplyResponse +): AppAfterSaleApplyResponse => ({ + ...payload, + afterSaleId: toId(payload.afterSaleId), +}) + export const normalizeAppHomeResponse = (payload: AppHomeResponse): AppHomeResponse => { const recommendMerchants = (payload.recommendMerchants ?? payload.recommendStores ?? []) .map(normalizeRecommendMerchant) @@ -289,6 +534,9 @@ export const normalizeAppHomeResponse = (payload: AppHomeResponse): AppHomeRespo return { ...payload, + banners: (payload.banners ?? []).map(normalizeBanner), + categoryNavs: (payload.categoryNavs ?? []).map(normalizeCategoryNav), + channels: (payload.channels ?? []).map(normalizeChannel), recommendGoods: payload.recommendGoods.map(normalizeGoodsSummary), recommendMerchants, featuredMerchants, @@ -305,7 +553,7 @@ export const normalizeAppHomeMerchantGoodsResponse = ( normalizeHomeMerchantGoodsBlock(payload.data) ?? ({ merchant: { - id: 0, + id: '0', merchantId: '0', storeName: '', storeLogo: '', @@ -319,7 +567,7 @@ export const normalizeAppHomeMerchantGoodsResponse = ( orderCount: 0, }, store: { - id: 0, + id: '0', merchantId: '0', storeName: '', storeLogo: '', @@ -354,7 +602,7 @@ export const normalizeAppTradePreviewResponse = ( ): AppTradePreviewResponse => { const data = payload.data as TradePreviewInfo & { uid?: number - memberId?: number + memberId?: MerchantId | number merchantGroups?: TradePreviewInfo['merchantGroups'] storeGroups?: TradePreviewInfo['merchantGroups'] } @@ -364,10 +612,24 @@ export const normalizeAppTradePreviewResponse = ( ...payload, data: { ...data, - uid: data.uid ?? data.memberId ?? 0, - memberId: data.memberId ?? data.uid, + addressId: toId(data.addressId), + uid: data.uid ?? 0, + memberId: data.memberId == null ? undefined : toMerchantId(data.memberId), merchantGroups: merchantGroups.map(normalizeTradePreviewGroup), storeGroups: merchantGroups.map(normalizeTradePreviewGroup), }, } } + +export const normalizeAppTradeCreateResponse = ( + payload: AppTradeCreateResponse +): AppTradeCreateResponse => ({ + ...payload, + tradeId: toId(payload.tradeId), + payUrl: pickText(payload.payUrl), + payExternal: toBoolean(payload.payExternal), + payExpiredAt: toUnixTime(payload.payExpiredAt), + payChannel: pickText(payload.payChannel), + paySubject: pickText(payload.paySubject), + orderIds: (payload.orderIds ?? []).map(toId), +}) diff --git a/src/apis/mall-app/order.ts b/src/apis/mall-app/order.ts index e6f5313..66b0d79 100644 --- a/src/apis/mall-app/order.ts +++ b/src/apis/mall-app/order.ts @@ -1,6 +1,7 @@ import { APP_API_PATHS, normalizeAppOrderDetailResponse, + normalizeAppOrderConfirmResponse, normalizeAppOrderPageResponse, normalizeMallAppPayload, postMallApp, @@ -40,9 +41,15 @@ export const apiAppOrderDetail = async (payload: AppOrderDetailRequest, signal?: } } -export const apiAppOrderConfirm = (payload: AppOrderConfirmRequest, signal?: AbortSignal) => - postMallApp( +export const apiAppOrderConfirm = async (payload: AppOrderConfirmRequest, signal?: AbortSignal) => { + const response = await postMallApp( APP_API_PATHS.orderConfirm, normalizeMallAppPayload(payload), signal ) + + return { + ...response, + data: normalizeAppOrderConfirmResponse(response.data), + } +} diff --git a/src/apis/mall-app/shared.ts b/src/apis/mall-app/shared.ts index 3467ce6..5a008db 100644 --- a/src/apis/mall-app/shared.ts +++ b/src/apis/mall-app/shared.ts @@ -11,6 +11,10 @@ export const APP_API_PATHS = { cartList: '/api/v1/mall/app/cart_list', cartSave: '/api/v1/mall/app/cart_save', cartRemove: '/api/v1/mall/app/cart_remove', + addressList: '/api/v1/mall/app/address_list', + addressSave: '/api/v1/mall/app/address_save', + addressDelete: '/api/v1/mall/app/address_delete', + addressDefaultSet: '/api/v1/mall/app/address_default_set', tradePreview: '/api/v1/mall/app/trade_preview', tradeCreate: '/api/v1/mall/app/trade_create', orderPage: '/api/v1/mall/app/order_page', diff --git a/src/apis/mall-app/trade.ts b/src/apis/mall-app/trade.ts index 22e861d..e2082f5 100644 --- a/src/apis/mall-app/trade.ts +++ b/src/apis/mall-app/trade.ts @@ -1,5 +1,6 @@ import { APP_API_PATHS, + normalizeAppTradeCreateResponse, normalizeAppTradePreviewResponse, normalizeMallAppPayload, postMallApp, @@ -24,9 +25,15 @@ export const apiAppTradePreview = async (payload: AppTradePreviewRequest, signal } } -export const apiAppTradeCreate = (payload: AppTradeCreateRequest, signal?: AbortSignal) => - postMallApp( +export const apiAppTradeCreate = async (payload: AppTradeCreateRequest, signal?: AbortSignal) => { + const response = await postMallApp( APP_API_PATHS.tradeCreate, normalizeMallAppPayload(payload), signal ) + + return { + ...response, + data: normalizeAppTradeCreateResponse(response.data), + } +} diff --git a/src/index.css b/src/index.css index 0326ad9..9d651c7 100644 --- a/src/index.css +++ b/src/index.css @@ -1976,6 +1976,21 @@ input::placeholder { font-size: 1.2rem; } +.market-member-panel__actions { + display: flex; + gap: 12px; + align-items: center; +} + +.market-member-panel__actions button { + min-height: 36px; + padding: 0 14px; + border-radius: var(--market-radius); + background: var(--market-brand); + color: #fff; + font-weight: 800; +} + .market-member-login-link { display: inline-flex; justify-content: center; @@ -1988,6 +2003,191 @@ input::placeholder { font-weight: 700; } +.market-address-editor { + display: grid; + gap: 16px; + margin-bottom: 18px; + padding: 18px; + border: 1px solid rgba(16, 128, 90, 0.18); + border-radius: var(--market-radius-lg); + background: linear-gradient(135deg, rgba(16, 128, 90, 0.08), #fff 46%); +} + +.market-address-editor__head { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; +} + +.market-address-editor__head h2 { + margin: 2px 0 0; + color: #111827; + font-size: 1.02rem; +} + +.market-address-editor__head > button { + color: #9ca3af; + font-size: 1.5rem; + line-height: 1; +} + +.market-address-editor__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.market-address-editor__grid label { + display: grid; + gap: 6px; + color: #111827; + font-weight: 700; +} + +.market-address-editor__grid label span { + color: #4b5563; + font-size: 0.84rem; +} + +.market-address-editor__grid input { + min-height: 38px; + padding: 0 12px; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: var(--market-radius); + background: #fff; + color: #111827; + font: inherit; +} + +.market-address-editor__full { + grid-column: 1 / -1; +} + +.market-address-editor__actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.market-address-editor__actions button { + min-height: 36px; + padding: 0 16px; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: var(--market-radius); + color: #111827; + font-weight: 800; +} + +.market-address-editor__actions button:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.market-address-editor__actions .market-address-editor__primary { + border-color: transparent; + background: var(--market-brand); + color: #fff; +} + +.market-address-list { + display: grid; + gap: 12px; +} + +.market-address-card { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: center; + padding: 18px; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: var(--market-radius-lg); + background: #fff; +} + +.market-address-card.is-default { + border-color: rgba(16, 128, 90, 0.26); + background: linear-gradient(135deg, rgba(16, 128, 90, 0.08), #fff 44%); +} + +.market-address-card__main { + display: grid; + gap: 10px; + min-width: 0; +} + +.market-address-card__head { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +.market-address-card__head strong { + color: #111827; + font-size: 1rem; +} + +.market-address-card__head span { + color: #4b5563; +} + +.market-address-card__head em { + border-radius: 999px; + padding: 2px 8px; + background: var(--market-brand-bg); + color: var(--market-brand); + font-style: normal; + font-size: 0.78rem; + font-weight: 800; +} + +.market-address-card p { + margin: 0; + color: #111827; + line-height: 1.55; +} + +.market-address-card__meta { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.market-address-card__meta span { + border-radius: 999px; + padding: 3px 8px; + background: #f5f6f8; + color: var(--market-muted); + font-size: 0.78rem; +} + +.market-address-card__actions { + display: flex; + gap: 10px; + align-items: center; +} + +.market-address-card__actions button { + min-height: 34px; + padding: 0 12px; + border: 1px solid rgba(16, 128, 90, 0.26); + border-radius: var(--market-radius); + color: var(--market-brand); + font-weight: 800; +} + +.market-address-card__actions button:disabled { + cursor: not-allowed; + opacity: 0.58; +} + +.market-address-card__actions button.is-danger { + border-color: transparent; + color: #ef4444; +} + .market-cart-list, .market-order-list { display: grid; @@ -2070,6 +2270,982 @@ input::placeholder { font-weight: 700; } +.market-cart-groups { + display: grid; + gap: 0; + overflow: hidden; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: var(--market-radius-lg); + background: #fff; +} + +.market-cart-merchant { + display: grid; + gap: 0; +} + +.market-cart-merchant + .market-cart-merchant { + border-top: 18px solid #f5f6f8; +} + +.market-cart-merchant__head { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 56px; + padding: 0 22px; + background: #f5f6f8; +} + +.market-cart-merchant__head label, +.market-cart-footer label { + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.market-cart-merchant__head strong { + color: #111827; + font-size: 0.98rem; +} + +.market-cart-merchant__head button, +.market-cart-sku-row__remove { + color: #ef4444; + font-weight: 700; +} + +.market-cart-merchant input, +.market-cart-goods input, +.market-cart-sku-row input, +.market-cart-footer input { + width: 16px; + height: 16px; + accent-color: #ef4444; +} + +.market-cart-goods-list { + display: grid; + gap: 0; +} + +.market-cart-goods { + display: grid; + grid-template-columns: minmax(280px, 0.34fr) minmax(0, 1fr); + gap: 18px; + align-items: start; + padding: 26px 22px; +} + +.market-cart-goods + .market-cart-goods { + border-top: 1px solid rgba(15, 23, 42, 0.08); +} + +.market-cart-goods__summary { + display: grid; + grid-template-columns: auto 88px minmax(0, 1fr); + gap: 14px; + align-items: start; + min-width: 0; +} + +.market-cart-goods__media img, +.market-cart-goods__media .market-media-placeholder { + width: 88px; + height: 88px; + border-radius: 0; + object-fit: cover; +} + +.market-cart-goods__body { + display: grid; + gap: 8px; + min-width: 0; +} + +.market-cart-goods__body a { + overflow: hidden; + color: #111827; + font-size: 1rem; + font-weight: 700; + line-height: 1.45; + text-overflow: ellipsis; + white-space: nowrap; +} + +.market-cart-goods__body span { + width: max-content; + border-radius: 6px; + padding: 2px 6px; + background: #ef4444; + color: #fff; + font-size: 0.78rem; + font-weight: 700; +} + +.market-cart-sku-table { + display: grid; + gap: 0; + min-width: 0; +} + +.market-cart-sku-table__head, +.market-cart-sku-row { + display: grid; + grid-template-columns: minmax(300px, 2.4fr) 92px 132px 112px; + gap: 18px; + align-items: center; +} + +.market-cart-sku-table__head { + min-height: 34px; + border-bottom: 1px solid rgba(15, 23, 42, 0.12); + color: #111827; + font-weight: 700; +} + +.market-cart-sku-table__head button { + justify-self: center; + color: #ef4444; + font: inherit; + font-weight: 800; +} + +.market-cart-sku-row { + min-height: 64px; + color: #111827; +} + +.market-cart-sku-row__spec { + display: inline-grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; + align-items: center; + min-width: 0; +} + +.market-cart-sku-row__spec span { + min-width: 0; + overflow: hidden; + display: -webkit-box; + font-size: 0.86rem; + line-height: 1.45; + text-overflow: ellipsis; + word-break: break-word; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.market-cart-sku-row strong { + color: #ef4444; +} + +.market-cart-sku-row__remove { + justify-self: center; +} + +.market-cart-sku-row__quantity { + display: grid; + grid-template-columns: 28px 44px 28px; + overflow: hidden; + width: 100px; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 7px; +} + +.market-cart-sku-row__quantity button, +.market-cart-sku-row__quantity span { + display: grid; + place-items: center; + min-height: 30px; +} + +.market-cart-sku-row__quantity span { + border-inline: 1px solid rgba(15, 23, 42, 0.08); +} + +.market-cart-footer { + position: sticky; + bottom: 0; + z-index: 2; + display: grid; + grid-template-columns: minmax(140px, 1fr) auto auto auto; + gap: 24px; + align-items: center; + min-height: 68px; + padding: 0 20px; + border-top: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(10px); +} + +.market-cart-footer > span { + color: #111827; +} + +.market-cart-footer > strong { + color: #ef4444; + font-size: 1.05rem; +} + +.market-cart-footer > button { + min-height: 42px; + padding: 0 24px; + border-radius: var(--market-radius); + background: #ef4444; + color: #fff; + font-weight: 800; +} + +.market-checkout-modal { + position: fixed; + inset: 0; + z-index: 55; + display: grid; + place-items: start center; + overflow: auto; + padding: 42px 20px; + background: rgba(17, 24, 39, 0.56); +} + +.market-checkout-dialog { + position: relative; + display: grid; + width: min(100%, 1480px); + max-height: calc(100vh - 84px); + overflow: auto; + border-radius: 18px; + background: #fff; + box-shadow: 0 28px 90px rgba(15, 23, 42, 0.28); +} + +.market-checkout-dialog__head { + position: sticky; + top: 0; + z-index: 3; + display: flex; + align-items: center; + justify-content: space-between; + min-height: 58px; + padding: 0 24px; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(12px); +} + +.market-checkout-dialog__head h2 { + margin: 0; + color: #111827; + font-size: 1.08rem; +} + +.market-checkout-dialog__head button { + color: #8b9099; + font-size: 1.75rem; + line-height: 1; +} + +.market-checkout-products { + display: grid; + padding: 18px 24px 0; +} + +.market-checkout-products__head, +.market-checkout-product { + display: grid; + grid-template-columns: minmax(330px, 0.9fr) minmax(560px, 1.45fr) 120px 140px; + gap: 28px; + align-items: start; +} + +.market-checkout-products__head { + min-height: 48px; + align-items: center; + border: 1px solid rgba(15, 23, 42, 0.06); + background: #fafafa; + color: #4b5563; + font-weight: 800; + text-align: center; +} + +.market-checkout-product { + padding: 22px 0; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); +} + +.market-checkout-merchant { + display: grid; +} + +.market-checkout-merchant + .market-checkout-merchant { + border-top: 16px solid #f5f6f8; +} + +.market-checkout-merchant__options { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 18px; + min-height: 56px; + padding: 10px 18px; + background: #f8fafc; +} + +.market-checkout-merchant__options strong { + color: #111827; + font-size: 0.98rem; +} + +.market-checkout-merchant__options label { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.market-checkout-merchant__options label span, +.market-checkout-merchant__options > span { + color: var(--market-muted); + font-size: 0.86rem; +} + +.market-checkout-merchant__options select { + height: 34px; + min-width: 150px; + border: 1px solid rgba(15, 23, 42, 0.14); + border-radius: 7px; + padding: 0 10px; + color: #111827; + font-size: 0.88rem; +} + +.market-checkout-product__attr { + display: grid; + grid-template-columns: 102px minmax(0, 1fr); + gap: 16px; + align-items: start; + padding-left: 18px; +} + +.market-checkout-product__media img, +.market-checkout-product__media .market-media-placeholder { + width: 102px; + height: 102px; + border-radius: 0; + object-fit: cover; +} + +.market-checkout-product__attr div { + display: grid; + gap: 8px; + min-width: 0; + color: #111827; +} + +.market-checkout-product__attr span, +.market-checkout-product__attr strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.market-checkout-product__attr strong { + font-size: 0.98rem; +} + +.market-checkout-product__attr em { + width: max-content; + border-radius: 5px; + padding: 2px 6px; + background: #ef4444; + color: #fff; + font-size: 0.76rem; + font-style: normal; + font-weight: 800; +} + +.market-checkout-sku-table { + display: grid; + min-width: 0; +} + +.market-checkout-sku-table__head, +.market-checkout-sku-row { + display: grid; + grid-template-columns: minmax(250px, 1fr) 110px 110px 76px; + gap: 18px; + align-items: center; +} + +.market-checkout-sku-table__head { + min-height: 42px; + border-bottom: 1px solid rgba(15, 23, 42, 0.1); + color: #4b5563; + font-weight: 800; +} + +.market-checkout-sku-row { + min-height: 54px; + color: #111827; +} + +.market-checkout-sku-row > span { + min-width: 0; + overflow: hidden; + display: -webkit-box; + line-height: 1.35; + text-overflow: ellipsis; + word-break: break-word; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.market-checkout-sku-row strong, +.market-checkout-sku-row button, +.market-checkout-product__amount { + color: #ef4444; + font-weight: 800; +} + +.market-checkout-sku-row input { + width: 72px; + height: 32px; + border: 1px solid rgba(15, 23, 42, 0.16); + border-radius: 6px; + padding: 0 10px; + color: #111827; + font-size: 0.92rem; +} + +.market-checkout-product__count { + align-self: center; + color: #111827; + font-weight: 700; + text-align: center; +} + +.market-checkout-product__amount { + align-self: center; + font-size: 1.24rem; + text-align: center; +} + +.market-checkout-address { + display: grid; + gap: 24px; + padding: 0 24px 24px; +} + +.market-checkout-address h3 { + margin: 0 -24px; + padding: 18px 24px; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + background: #fafafa; + color: #374151; + font-size: 1rem; +} + +.market-checkout-address__form { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + gap: 18px 36px; +} + +.market-checkout-hint { + margin: 0 24px 18px; + border-radius: 10px; + padding: 10px 14px; + background: #f8fafc; + color: var(--market-muted); + font-size: 0.9rem; +} + +.market-checkout-hint.is-error { + background: #fff1f2; + color: #ef4444; +} + +.market-checkout-address__form label { + display: grid; + grid-template-columns: 100px minmax(0, 1fr); + gap: 12px; + align-items: center; +} + +.market-checkout-address__saved, +.market-checkout-address__detail { + grid-column: 1; +} + +.market-checkout-address__form label span { + color: #111827; + font-weight: 700; + text-align: right; +} + +.market-checkout-address__form label span::first-letter { + color: #ef4444; +} + +.market-checkout-address__form input, +.market-checkout-address__form select, +.market-checkout-address__form textarea { + width: 100%; + border: 1px solid rgba(15, 23, 42, 0.16); + border-radius: 8px; + color: #111827; + font-size: 0.94rem; +} + +.market-checkout-address__form input, +.market-checkout-address__form select { + height: 40px; + padding: 0 14px; +} + +.market-checkout-address__recognize { + grid-column: 2; + grid-row: 1 / span 4; + display: grid; + gap: 12px; + align-content: start; +} + +.market-checkout-address__recognize > div:first-child { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; +} + +.market-checkout-address__recognize strong { + color: #111827; +} + +.market-checkout-address__recognize span { + color: var(--market-muted); + font-size: 0.88rem; +} + +.market-checkout-address__recognize textarea { + min-height: 70px; + resize: vertical; + padding: 12px 14px; +} + +.market-checkout-address__recognize > div:last-child { + display: flex; + gap: 12px; +} + +.market-checkout-address__recognize button { + min-height: 40px; + padding: 0 18px; + border: 1px solid rgba(15, 23, 42, 0.14); + border-radius: 8px; + color: #111827; + font-weight: 700; +} + +.market-checkout-address__recognize button:disabled, +.market-checkout-footer button:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.market-checkout-address__recognize .market-checkout-address__primary { + border-color: transparent; + background: #ef4444; + color: #fff; +} + +.market-checkout-footer { + position: sticky; + bottom: 0; + z-index: 3; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 18px; + min-height: 72px; + padding: 12px 24px; + border-top: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(12px); +} + +.market-checkout-payments { + display: flex; + flex-wrap: wrap; + gap: 12px 18px; + margin-right: auto; +} + +.market-checkout-payments label, +.market-checkout-agreement { + display: inline-flex; + align-items: center; + gap: 8px; + color: #111827; +} + +.market-checkout-payments input, +.market-checkout-agreement input { + width: 16px; + height: 16px; + accent-color: #ef4444; +} + +.market-checkout-payments small { + color: #111827; +} + +.market-checkout-agreement a { + color: #1677ff; +} + +.market-checkout-footer strong { + color: #ef4444; + font-size: 1.08rem; +} + +.market-checkout-footer small { + color: var(--market-muted); + font-size: 0.84rem; +} + +.market-checkout-footer button { + min-height: 42px; + padding: 0 22px; + border: 1px solid rgba(15, 23, 42, 0.14); + border-radius: 8px; + color: #111827; + font-weight: 800; +} + +.market-checkout-footer__primary { + border-color: transparent !important; + background: #ef4444; + color: #fff !important; +} + +.market-pay-modal { + position: fixed; + inset: 0; + z-index: 70; + display: grid; + place-items: center; + padding: 24px; + background: rgba(15, 23, 42, 0.48); + backdrop-filter: blur(8px); +} + +.market-pay-dialog { + display: grid; + gap: 18px; + width: min(620px, 100%); + padding: 24px; + border-radius: var(--market-radius-xl); + background: #fff; + box-shadow: 0 28px 80px rgba(15, 23, 42, 0.28); +} + +.market-pay-dialog__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.market-pay-dialog__head h2 { + margin: 2px 0 0; + color: #111827; + font-size: 1.18rem; +} + +.market-pay-dialog__head button { + color: #9ca3af; + font-size: 1.5rem; + line-height: 1; +} + +.market-pay-dialog__summary { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.market-pay-dialog__summary article { + display: grid; + gap: 4px; + padding: 12px; + border-radius: var(--market-radius); + background: #f8fafc; +} + +.market-pay-dialog__summary span, +.market-pay-dialog__body span, +.market-pay-dialog__body small { + color: var(--market-muted); + font-size: 0.82rem; +} + +.market-pay-dialog__summary strong { + overflow: hidden; + color: #111827; + text-overflow: ellipsis; + white-space: nowrap; +} + +.market-pay-dialog__body { + display: grid; + gap: 8px; + padding: 14px; + border: 1px dashed rgba(16, 128, 90, 0.24); + border-radius: var(--market-radius-lg); + background: rgba(16, 128, 90, 0.05); +} + +.market-pay-dialog__body a { + overflow-wrap: anywhere; + color: var(--market-brand); + font-weight: 800; +} + +.market-pay-dialog__actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.market-pay-dialog__actions button { + min-height: 38px; + padding: 0 16px; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: var(--market-radius); + color: #111827; + font-weight: 800; +} + +.market-pay-dialog__actions .market-pay-dialog__primary { + border-color: transparent; + background: var(--market-brand); + color: #fff; +} + +.market-cart-add-modal { + position: fixed; + inset: 0; + z-index: 50; + display: grid; + place-items: start center; + overflow: auto; + padding: 64px 24px; + background: rgba(17, 24, 39, 0.52); +} + +.market-cart-add-dialog { + display: grid; + gap: 22px; + width: min(100%, 900px); + min-height: 560px; + padding: 26px 28px; + border-radius: 8px; + background: #fff; + box-shadow: 0 28px 80px rgba(15, 23, 42, 0.26); +} + +.market-cart-add-dialog__head, +.market-cart-add-dialog__foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.market-cart-add-dialog__head h2 { + margin: 0; + color: #111827; + font-size: 1.15rem; +} + +.market-cart-add-dialog__head button { + color: #8b9099; + font-size: 1.8rem; + line-height: 1; +} + +.market-cart-add-dialog__product { + display: grid; + grid-template-columns: 92px minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.market-cart-add-dialog__product img, +.market-cart-add-dialog__product .market-media-placeholder { + width: 92px; + height: 92px; + border-radius: 0; + object-fit: cover; +} + +.market-cart-add-dialog__product div { + display: grid; + gap: 8px; + min-width: 0; + color: #111827; +} + +.market-cart-add-dialog__product strong, +.market-cart-add-dialog__product span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.market-cart-add-dialog__product small { + color: #ef4444; + font-weight: 700; +} + +.market-cart-add-dialog__section-title { + display: inline-flex; + align-items: center; + gap: 10px; + color: #111827; +} + +.market-cart-add-dialog__section-title span { + width: 4px; + height: 24px; + border-radius: 999px; + background: #ef4444; +} + +.market-cart-add-color-tabs { + display: flex; + gap: 12px; + overflow-x: auto; + padding-bottom: 4px; +} + +.market-cart-add-color-tabs button { + display: grid; + gap: 4px; + min-width: 132px; + min-height: 54px; + padding: 8px 14px; + border: 1px solid rgba(15, 23, 42, 0.16); + border-radius: 7px; + color: #111827; + font-size: 0.92rem; + font-weight: 700; + text-align: left; + white-space: nowrap; +} + +.market-cart-add-color-tabs button.is-active { + border-color: #ef4444; + color: #ef4444; + box-shadow: inset 0 0 0 1px #ef4444; +} + +.market-cart-add-color-tabs small { + color: var(--market-muted); + font-size: 0.76rem; + font-weight: 500; +} + +.market-cart-add-color-tabs button.is-active small { + color: #ef4444; +} + +.market-cart-add-sku-list { + display: grid; + gap: 0; +} + +.market-cart-add-sku-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 120px 120px; + gap: 20px; + align-items: center; + min-height: 64px; +} + +.market-cart-add-sku-row + .market-cart-add-sku-row { + border-top: 1px solid rgba(15, 23, 42, 0.08); +} + +.market-cart-add-sku-row div { + display: grid; + gap: 4px; + min-width: 0; +} + +.market-cart-add-sku-row strong { + overflow: hidden; + color: #111827; + font-size: 0.96rem; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} + +.market-cart-add-sku-row span { + color: var(--market-muted); + font-size: 0.82rem; +} + +.market-cart-add-sku-row b { + color: #ef4444; + font-size: 1rem; +} + +.market-cart-add-sku-row input { + width: 100%; + height: 38px; + padding: 0 12px; + border: 1px solid rgba(15, 23, 42, 0.16); + border-radius: 7px; + color: #111827; + font-size: 0.95rem; +} + +.market-cart-add-dialog__foot { + align-self: end; + justify-content: flex-end; + margin-top: auto; + padding-top: 18px; +} + +.market-cart-add-dialog__foot span { + margin-right: auto; + color: #111827; +} + +.market-cart-add-dialog__foot button { + min-width: 82px; + min-height: 38px; + border: 1px solid rgba(15, 23, 42, 0.14); + border-radius: 7px; + color: #111827; + font-weight: 700; +} + +.market-cart-add-dialog__confirm { + border-color: transparent !important; + background: #ef4444; + color: #fff !important; +} + +.market-cart-add-dialog__confirm:disabled { + cursor: not-allowed; + background: #fca5a5; +} + .market-order-card { display: grid; gap: 14px; @@ -2134,6 +3310,272 @@ input::placeholder { color: #111827; } +.market-order-card__actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.market-order-card__actions button, +.market-order-detail-dialog__foot button, +.market-after-sale-dialog__foot button { + min-height: 36px; + padding: 0 14px; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 8px; + background: #fff; + color: #111827; + font-weight: 700; +} + +.market-order-card__actions button:disabled, +.market-order-detail-dialog__foot button:disabled, +.market-after-sale-dialog__foot button:disabled { + cursor: not-allowed; + opacity: 0.58; +} + +.market-order-card__primary, +.market-order-detail-dialog__primary, +.market-after-sale-dialog__primary { + border-color: transparent !important; + background: #ef4444 !important; + color: #fff !important; +} + +.market-order-detail-modal, +.market-after-sale-modal { + position: fixed; + inset: 0; + z-index: 58; + display: grid; + place-items: start center; + overflow: auto; + padding: 48px 20px; + background: rgba(17, 24, 39, 0.54); +} + +.market-order-detail-dialog, +.market-after-sale-dialog { + display: grid; + gap: 18px; + width: min(100%, 980px); + max-height: calc(100vh - 96px); + overflow: auto; + border-radius: 16px; + background: #fff; + box-shadow: 0 28px 88px rgba(15, 23, 42, 0.28); +} + +.market-order-detail-dialog__head, +.market-after-sale-dialog__head { + position: sticky; + top: 0; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 20px 24px 16px; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(12px); +} + +.market-order-detail-dialog__head h2, +.market-after-sale-dialog__head h2 { + margin: 0; + color: #111827; + font-size: 1.16rem; +} + +.market-order-detail-dialog__head span { + display: block; + margin-top: 4px; + color: var(--market-muted); + font-size: 0.86rem; +} + +.market-order-detail-dialog__head button, +.market-after-sale-dialog__head button { + color: #8b9099; + font-size: 1.8rem; + line-height: 1; +} + +.market-order-detail-summary { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; + padding: 0 24px; +} + +.market-order-detail-summary article { + display: grid; + gap: 6px; + padding: 14px 12px; + border-radius: 12px; + background: #f8fafc; +} + +.market-order-detail-summary span, +.market-order-detail-section p, +.market-order-detail-item small, +.market-after-sale-product small { + color: var(--market-muted); +} + +.market-order-detail-summary strong { + color: #111827; +} + +.market-order-detail-summary article:last-child strong { + color: #ef4444; +} + +.market-order-detail-section { + display: grid; + gap: 10px; + margin: 0 24px; + padding: 18px; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 14px; +} + +.market-order-detail-section h3 { + margin: 0; + color: #111827; + font-size: 1rem; +} + +.market-order-detail-section p { + margin: 0; + line-height: 1.7; +} + +.market-order-detail-items { + display: grid; + gap: 12px; +} + +.market-order-detail-item { + display: grid; + grid-template-columns: 72px minmax(0, 1fr) 72px 100px auto; + gap: 14px; + align-items: center; + padding: 12px; + border-radius: 12px; + background: #f8fafc; +} + +.market-order-detail-item img, +.market-after-sale-product img { + width: 72px; + height: 72px; + object-fit: cover; +} + +.market-order-detail-item div, +.market-after-sale-product div { + display: grid; + gap: 6px; + min-width: 0; +} + +.market-order-detail-item strong, +.market-order-detail-item span, +.market-after-sale-product strong, +.market-after-sale-product span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.market-order-detail-item > strong { + color: #ef4444; +} + +.market-order-detail-item button { + min-height: 34px; + padding: 0 12px; + border: 1px solid rgba(239, 68, 68, 0.28); + border-radius: 8px; + color: #ef4444; + font-weight: 700; +} + +.market-order-detail-item button:disabled { + cursor: not-allowed; + border-color: rgba(15, 23, 42, 0.1); + color: var(--market-muted); +} + +.market-order-detail-dialog__foot, +.market-after-sale-dialog__foot { + position: sticky; + bottom: 0; + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px 20px; + border-top: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(12px); +} + +.market-after-sale-dialog { + width: min(100%, 760px); +} + +.market-after-sale-product { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 16px; + align-items: center; + padding: 0 24px; +} + +.market-after-sale-form { + display: grid; + gap: 14px; + padding: 0 24px; +} + +.market-after-sale-form label { + display: grid; + grid-template-columns: 88px minmax(0, 1fr); + gap: 12px; + align-items: center; +} + +.market-after-sale-form label span { + color: #111827; + font-weight: 700; +} + +.market-after-sale-form input, +.market-after-sale-form select, +.market-after-sale-form textarea { + width: 100%; + border: 1px solid rgba(15, 23, 42, 0.14); + border-radius: 8px; + color: #111827; + font-size: 0.94rem; +} + +.market-after-sale-form input, +.market-after-sale-form select { + height: 38px; + padding: 0 12px; +} + +.market-after-sale-form textarea { + min-height: 86px; + resize: vertical; + padding: 10px 12px; +} + .market-media-placeholder { min-height: 100%; border-radius: 14px; @@ -2610,6 +4052,11 @@ input::placeholder { background: #ef4444; } +.market-detail-sku button:disabled { + cursor: not-allowed; + background: #d1d5db; +} + .market-detail-sku b { display: grid; place-items: center; @@ -2620,6 +4067,45 @@ input::placeholder { font-weight: 500; } +.market-detail-cart-actions { + display: flex; + align-items: center; + gap: 12px; + padding-top: 6px; + flex-wrap: wrap; +} + +.market-detail-cart-actions button { + min-height: 42px; + padding: 0 22px; + border-radius: var(--market-radius); + font-size: 0.95rem; + font-weight: 800; +} + +.market-detail-cart-actions__primary { + background: #ef4444; + color: #fff; + box-shadow: 0 12px 22px rgba(239, 68, 68, 0.18); +} + +.market-detail-cart-actions__primary:disabled { + cursor: not-allowed; + background: #fca5a5; + box-shadow: none; +} + +.market-detail-cart-actions__ghost { + border: 1px solid rgba(239, 68, 68, 0.3); + background: #fff; + color: #ef4444; +} + +.market-detail-cart-actions span { + color: var(--market-muted); + font-size: 0.88rem; +} + .market-detail-merchant { display: grid; gap: 14px; @@ -3215,7 +4701,7 @@ input::placeholder { } .market-member-tabs { - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); } .market-member-tabs button { @@ -3233,10 +4719,48 @@ input::placeholder { justify-self: start; } + .market-cart-goods { + grid-template-columns: 1fr; + } + + .market-cart-sku-table__head, + .market-cart-sku-row { + grid-template-columns: minmax(220px, 2.2fr) 76px 112px 72px; + } + + .market-cart-footer { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .market-cart-footer > button { + justify-self: start; + } + .market-order-card__body { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .market-order-detail-summary { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .market-address-card { + grid-template-columns: 1fr; + } + + .market-address-card__actions { + justify-content: flex-start; + } + + .market-order-detail-item { + grid-template-columns: 64px minmax(0, 1fr) 56px 88px; + } + + .market-order-detail-item button { + grid-column: 2 / -1; + justify-self: start; + } + .market-product-grid { grid-template-columns: repeat(auto-fill, minmax(204px, 204px)); } @@ -3299,6 +4823,15 @@ input::placeholder { flex-direction: column; } + .market-member-panel__actions { + width: 100%; + justify-content: space-between; + } + + .market-address-editor__grid { + grid-template-columns: 1fr; + } + .market-search__field, .login-inline-field { grid-template-columns: 1fr; @@ -3309,6 +4842,23 @@ input::placeholder { grid-template-columns: 1fr; } + .market-order-detail-modal, + .market-after-sale-modal { + padding: 24px 10px; + } + + .market-order-detail-summary, + .market-order-detail-item, + .market-after-sale-form label { + grid-template-columns: 1fr; + } + + .market-order-detail-item img, + .market-after-sale-product img { + width: 64px; + height: 64px; + } + .market-member-panel { padding: 16px; } @@ -3325,6 +4875,45 @@ input::placeholder { height: 64px; } + .market-cart-merchant__head { + padding: 0 14px; + } + + .market-cart-goods { + padding: 18px 14px; + } + + .market-cart-goods__summary { + grid-template-columns: auto 72px minmax(0, 1fr); + } + + .market-cart-goods__media img, + .market-cart-goods__media .market-media-placeholder { + width: 72px; + height: 72px; + } + + .market-cart-sku-table__head { + display: none; + } + + .market-cart-sku-row { + grid-template-columns: 1fr; + gap: 8px; + padding: 12px 0; + border-top: 1px solid rgba(15, 23, 42, 0.08); + } + + .market-cart-sku-row__quantity { + width: 112px; + } + + .market-cart-footer { + grid-template-columns: 1fr; + gap: 10px; + padding: 14px; + } + .market-search__core { grid-template-columns: 1fr; } diff --git a/src/mocks/mall.ts b/src/mocks/mall.ts index 23cafef..b2f0001 100644 --- a/src/mocks/mall.ts +++ b/src/mocks/mall.ts @@ -152,10 +152,10 @@ const buildGoods = (seed: string[], index: number): GoodsSummary => { const price = 29 + index * 7 return { - id: 10001 + index, + id: String(10001 + index), merchantId: String(5001 + (index % 4)), - storeId: 6001 + (index % 5), - categoryId: index + 1, + storeId: String(6001 + (index % 5)), + categoryId: String(index + 1), categoryName: category.label, spuNo: `YTSPU${String(index + 1).padStart(4, '0')}`, spuName: seed[0], @@ -181,7 +181,7 @@ const buildStore = (seed: string[], index: number): RecommendMerchantInfo => { const category = MARKET_CATEGORIES[(index % (MARKET_CATEGORIES.length - 1)) + 1] return { - id: 9001 + index, + id: String(9001 + index), merchantId: String(5001 + index), storeName: seed[0], storeLogo: createArtwork(seed[0], seed[1], category.accent, category.accentSoft), @@ -206,14 +206,14 @@ export const FALLBACK_GOODS_PAGE: AppGoodsPageResponse = { export const FALLBACK_HOME: AppHomeResponse = { categoryNavs: MARKET_CATEGORIES.map((item, index) => ({ - categoryId: index + 1, + categoryId: String(index + 1), categoryName: item.label, goodsCount: Math.max(0, goodsList.length - index), sort: index, })), banners: [ { - id: 1, + id: '1', title: '精准搜款 · 一件代发', subTitle: '多平台供给与商家资源聚合', description: '首页主视觉广告栏', @@ -228,7 +228,7 @@ export const FALLBACK_HOME: AppHomeResponse = { endTime: 1730000000, }, { - id: 2, + id: '2', title: '热卖数码专场', subTitle: '快充、支架、耳机同步起量', description: '首页活动广告栏', @@ -245,7 +245,7 @@ export const FALLBACK_HOME: AppHomeResponse = { ], searchKeywords: HOT_KEYWORDS, channels: PLATFORM_CHANNELS.map((item, index) => ({ - id: index + 1, + id: String(index + 1), title: item.label, subTitle: item.subtitle, iconUrl: '', diff --git a/src/pages/goods/GoodsDetailPage.tsx b/src/pages/goods/GoodsDetailPage.tsx index 7c990c8..930dab0 100644 --- a/src/pages/goods/GoodsDetailPage.tsx +++ b/src/pages/goods/GoodsDetailPage.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import { apiAppGoodsDetail } from '@/apis' +import { toast } from 'sonner' +import { apiAppCartSave, apiAppGoodsDetail } from '@/apis' import MarketPageHeader from '@/components/MarketPageHeader' import { useUserStore } from '@/store/user' import type { @@ -58,12 +59,12 @@ const WHITE_IMAGE_KEYWORDS = ['white', 'wash', 'clean', 'plain', 'background', ' const getSpecValue = (sku: ResolvedGoodsSkuInfo, patterns: string[]) => { const matched = sku.specs.find(spec => { - const name = typeof spec.name === 'string' ? spec.name : '' + const name = typeof spec.attributeName === 'string' ? spec.attributeName : '' return patterns.some(pattern => name.toLowerCase().includes(pattern.toLowerCase())) }) - return typeof matched?.value === 'string' ? matched.value.trim() : '' + return typeof matched?.attributeValue === 'string' ? matched.attributeValue.trim() : '' } const splitSkuName = (skuName: string) => @@ -175,14 +176,18 @@ const getProductParams = (detail: ResolvedGoodsDetailInfo): ParamItem[] => { ].filter(item => item.value) } +const getSelectedCartItems = (skus: ResolvedGoodsSkuInfo[], quantities: Record) => + skus + .map(sku => ({ + sku, + buyNum: Math.min(Math.max(0, quantities[sku.id] ?? 0), sku.stock), + })) + .filter(item => item.buyNum > 0) + function GoodsDetailPage() { const { spuId = '' } = useParams() const navigate = useNavigate() const token = useUserStore(state => state.token) - const userInfo = useUserStore(state => state.userInfo) - const storedMemberId = useUserStore(state => state.memberId) - - const currentUid = userInfo?.uid ?? storedMemberId ?? token?.memberId ?? token?.uid ?? 0 const [payload, setPayload] = useState(null) const [loading, setLoading] = useState(true) @@ -191,7 +196,8 @@ function GoodsDetailPage() { const [keyword, setKeyword] = useState('') const [selectedColor, setSelectedColor] = useState('') const [activeDetailTab, setActiveDetailTab] = useState('detail') - const [quantities, setQuantities] = useState>({}) + const [quantities, setQuantities] = useState>({}) + const [cartSaving, setCartSaving] = useState(false) useEffect(() => { if (!spuId) { @@ -204,7 +210,7 @@ function GoodsDetailPage() { setLoading(true) setError(null) - apiAppGoodsDetail({ uid: currentUid, spuId }, controller.signal) + apiAppGoodsDetail({ spuId }, controller.signal) .then(response => { setPayload(response.data) }) @@ -220,7 +226,7 @@ function GoodsDetailPage() { }) return () => controller.abort() - }, [currentUid, spuId]) + }, [spuId]) const detail = useMemo(() => (payload?.data ? resolveGoodsDetailInfo(payload.data) : null), [payload]) const merchant = payload?.merchant ?? payload?.store ?? null @@ -273,6 +279,14 @@ function GoodsDetailPage() { return galleryImages }, [detailImages, galleryImages]) const whiteImages = useMemo(() => (detail ? getWhiteImageList(detail) : []), [detail]) + const selectedCartItems = useMemo( + () => getSelectedCartItems(detail?.skus ?? [], quantities), + [detail, quantities] + ) + const selectedCartTotal = useMemo( + () => selectedCartItems.reduce((total, item) => total + item.buyNum, 0), + [selectedCartItems] + ) useEffect(() => { setActiveImage(galleryImages[0] ?? '') @@ -282,9 +296,9 @@ function GoodsDetailPage() { setSelectedColor(colorOptions[0]?.color ?? '') }, [colorOptions]) - const updateQuantity = (skuId: number, delta: number) => { + const updateQuantity = (skuId: string, delta: number, stock: number) => { setQuantities(current => { - const nextValue = Math.max(0, (current[skuId] ?? 0) + delta) + const nextValue = Math.min(stock, Math.max(0, (current[skuId] ?? 0) + delta)) return { ...current, [skuId]: nextValue, @@ -292,6 +306,36 @@ function GoodsDetailPage() { }) } + const handleAddCart = async () => { + if (!token?.accessToken) { + navigate('/login') + return + } + + if (selectedCartItems.length === 0) { + toast.warning('请先选择要加入购物车的商品数量') + return + } + + try { + setCartSaving(true) + await Promise.all( + selectedCartItems.map(item => + apiAppCartSave({ + skuId: item.sku.id, + buyNum: item.buyNum, + checked: true, + }) + ) + ) + toast.success(`已加入购物车 ${selectedCartTotal} 件商品`) + } catch (nextError) { + toast.error(nextError instanceof Error ? nextError.message : '加入购物车失败') + } finally { + setCartSaving(false) + } + } + const handleHeaderSearch = (nextKeyword: string) => { const trimmedKeyword = nextKeyword.trim() navigate(trimmedKeyword ? `/list/new?key=${encodeURIComponent(trimmedKeyword)}` : '/list/new') @@ -425,11 +469,15 @@ function GoodsDetailPage() { {formatMoney(option.sku.salePrice || detail.minPrice)} 库存 {option.sku.stock}
- {quantities[option.sku.id] ?? 0} -
@@ -437,10 +485,31 @@ function GoodsDetailPage() { ))} - ) : ( -
暂无 SKU 信息
- )} - + ) : ( +
暂无 SKU 信息
+ )} + + {detail.skus.length > 0 ? ( +
+ + + {selectedCartTotal > 0 ? 已选 {selectedCartTotal} 件 : null} +
+ ) : null} +