diff --git a/src/components/PriceText/index.less b/src/components/PriceText/index.less
new file mode 100644
index 0000000..d1fca35
--- /dev/null
+++ b/src/components/PriceText/index.less
@@ -0,0 +1,21 @@
+// ===== 零售价标注:小字置灰 =====
+.price-text__retail {
+ font-size: 20rpx;
+ color: #969799;
+ font-weight: 400;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+// inline 模式:与大价格同行,左间距分隔
+.price-text__retail--inline {
+ margin-left: 8rpx;
+}
+
+// block 模式:大价格 / 零售价上下两行(窄卡片布局)
+.price-text {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ line-height: 1.3;
+}
diff --git a/src/components/PriceText/index.tsx b/src/components/PriceText/index.tsx
new file mode 100644
index 0000000..f2afd3d
--- /dev/null
+++ b/src/components/PriceText/index.tsx
@@ -0,0 +1,47 @@
+import { Text, View } from '@tarojs/components'
+import { formatRetailPrice } from '@/utils/format'
+import './index.less'
+
+interface PriceTextProps {
+ /** 售价(展示为 ¥price) */
+ price: string | number
+ /** 包规(用于计算零售价;无法计算时不展示零售价) */
+ spec?: string | number | null
+ /** 大价格样式类(字号 / 颜色由调用方控制) */
+ className?: string
+ /**
+ * 零售价布局:
+ * - inline 跟随大价格同行(宽裕区域:商品详情、各类弹层行)
+ * - block 独占一行(窄卡片:首页推荐、商品列表、购物车)
+ */
+ mode?: 'inline' | 'block'
+}
+
+/**
+ * 商品价格:售价 + 零售价标注(小字)
+ * price=30、spec=15 → ¥30 零售价:¥2
+ */
+export default function PriceText({ price, spec, className, mode = 'inline' }: PriceTextProps) {
+ const retail = formatRetailPrice(price, spec)
+
+ // 窄卡片:大价格 / 零售价上下两行,避免与右侧按钮(+/步进器)挤压换行
+ if (mode === 'block') {
+ return (
+
+ ¥{price}
+ {retail !== null && (
+ 零售价:¥{retail}
+ )}
+
+ )
+ }
+
+ return (
+
+ ¥{price}
+ {retail !== null && (
+ 零售价:¥{retail}
+ )}
+
+ )
+}
diff --git a/src/pages/bill-detail/index.tsx b/src/pages/bill-detail/index.tsx
index ea91a4a..2930d8c 100644
--- a/src/pages/bill-detail/index.tsx
+++ b/src/pages/bill-detail/index.tsx
@@ -5,7 +5,8 @@ import { Empty, Popup } from '@antmjs/vantui'
import { getBillDetailApi } from '@/services/bill'
import { getOrderDetailApi } from '@/services/order'
import { ORDER_STATUS_TEXT } from '@/types/order'
-import { resolveFileUrl } from '@/utils/format'
+import { formatSpec, resolveFileUrl } from '@/utils/format'
+import PriceText from '@/components/PriceText'
import type { OrderDetail } from '@/types/order'
import type { BillDetail } from '@/services/bill'
import './index.less'
@@ -161,7 +162,8 @@ export default function BillDetailPage() {
{item.product_name}
- {item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit}
+ {formatSpec(item.product_spec, item.unit)}{' '}
+
@@ -214,7 +216,8 @@ export default function BillDetailPage() {
{item.product_name}
- {item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity}
+ {formatSpec(item.product_spec, item.unit)}{' '}
+ × {item.quantity}
¥{item.amount}
diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx
index 0db53b3..c5b6ba3 100644
--- a/src/pages/cart/index.tsx
+++ b/src/pages/cart/index.tsx
@@ -5,6 +5,8 @@ import { Button, Empty, Icon, Popup, Stepper } from '@antmjs/vantui'
import useCartStore from '@/stores/cart/useCartStore'
import { createOrderApi } from '@/services/order'
import { getStoreInfoApi } from '@/services/store'
+import PriceText from '@/components/PriceText'
+import { formatSpec } from '@/utils/format'
import type { CartItem } from '@/types/cart'
import type { StoreDetail } from '@/types/store'
import './index.less'
@@ -205,10 +207,10 @@ export default function CartPage() {
{item.name}
{item.status === 0 && 已失效}
- {item.spec} / {item.unit}
+ {formatSpec(item.spec, item.unit)}
{item.price !== null ? (
- ¥{item.price}
+
) : (
价格待定
)}
@@ -302,7 +304,7 @@ export default function CartPage() {
{item.name}
- {item.spec} / {item.unit}
+ {formatSpec(item.spec, item.unit)}
diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx
index 0a00ae1..48aa728 100644
--- a/src/pages/index/index.tsx
+++ b/src/pages/index/index.tsx
@@ -9,6 +9,8 @@ import type { HomeConfig } from '@/services/home'
import { getProductListApi } from '@/services/product'
import { getProductCover } from '@/types/product'
import type { Product } from '@/types/product'
+import PriceText from '@/components/PriceText'
+import { formatSpec } from '@/utils/format'
import './index.less'
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
@@ -247,10 +249,10 @@ export default function IndexPage() {
/>
{product.name}
- {product.spec} / {product.unit}
+ {formatSpec(product.spec, product.unit)}
{product.price !== null ? (
- ¥{product.price}
+
) : (
登陆后查看价格
)}
diff --git a/src/pages/order-list/index.tsx b/src/pages/order-list/index.tsx
index 6c2623d..807d057 100644
--- a/src/pages/order-list/index.tsx
+++ b/src/pages/order-list/index.tsx
@@ -5,7 +5,8 @@ import { Empty, Popup } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
import { ORDER_STATUS_FILTERS, ORDER_STATUS_TEXT } from '@/types/order'
-import { resolveFileUrl } from '@/utils/format'
+import { formatSpec, resolveFileUrl } from '@/utils/format'
+import PriceText from '@/components/PriceText'
import type { OrderDetail, OrderListItem, OrderStatus } from '@/types/order'
import './index.less'
@@ -184,8 +185,8 @@ export default function OrderListPage() {
)}
{i.product_name}
- {!!i.product_spec && (
- {i.product_spec} {i.unit}
+ {!!formatSpec(i.product_spec, i.unit) && (
+ {formatSpec(i.product_spec, i.unit)}
)}
×{i.quantity}
@@ -262,7 +263,8 @@ export default function OrderListPage() {
{item.product_name}
- {item.product_spec ? `${item.product_spec}${item.unit} ` : ''}¥{item.price} × {item.quantity}
+ {formatSpec(item.product_spec, item.unit)}{' '}
+ × {item.quantity}
¥{item.amount}
diff --git a/src/pages/product-detail/index.less b/src/pages/product-detail/index.less
index 3a483cb..05b6a48 100644
--- a/src/pages/product-detail/index.less
+++ b/src/pages/product-detail/index.less
@@ -61,12 +61,6 @@
}
}
- &__unit {
- margin-left: 8rpx;
- font-size: 24rpx;
- color: #969799;
- }
-
&__name {
display: block;
margin-top: 16rpx;
diff --git a/src/pages/product-detail/index.tsx b/src/pages/product-detail/index.tsx
index 168ad0b..2b28652 100644
--- a/src/pages/product-detail/index.tsx
+++ b/src/pages/product-detail/index.tsx
@@ -5,7 +5,8 @@ import { Empty, Stepper, Swiper, SwiperItem } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import useCartStore from '@/stores/cart/useCartStore'
import { getProductDetailApi } from '@/services/product'
-import { resolveFileUrl } from '@/utils/format'
+import { formatSpec, resolveFileUrl } from '@/utils/format'
+import PriceText from '@/components/PriceText'
import type { Product } from '@/types/product'
import './index.less'
@@ -115,16 +116,15 @@ export default function ProductDetailPage() {
{product.price !== null ? (
- ¥{product.price}
+
) : (
{loggedIn ? '价格待定' : '登录后查看价格'}
)}
- /{product.unit}
{product.name}
- {product.spec}
+ {formatSpec(product.spec, product.unit)}
{!!product.shelf_life && product.shelf_life > 0 && (
保质期 {product.shelf_life} 天
diff --git a/src/pages/product/index.tsx b/src/pages/product/index.tsx
index 961b03d..b147753 100644
--- a/src/pages/product/index.tsx
+++ b/src/pages/product/index.tsx
@@ -7,6 +7,8 @@ import { getCategoriesApi, getProductListApi } from '@/services/product'
import type { ProductListParams } from '@/services/product'
import { getProductCover } from '@/types/product'
import type { Category, Product } from '@/types/product'
+import PriceText from '@/components/PriceText'
+import { formatSpec } from '@/utils/format'
import './index.less'
const PAGE_SIZE = 10
@@ -288,10 +290,10 @@ export default function ProductPage() {
/>
{product.name}
- {product.spec} / {product.unit}
+ {formatSpec(product.spec, product.unit)}
{product.price !== null ? (
- ¥{product.price}
+
) : (
价格待定
)}
@@ -339,9 +341,9 @@ export default function ProductPage() {
/>
{current.name}
- {current.spec} / {current.unit}
+ {formatSpec(current.spec, current.unit)}
{current.price !== null ? (
- ¥{current.price}
+
) : (
价格待定
)}
diff --git a/src/utils/format.ts b/src/utils/format.ts
index 61b47d0..6279767 100644
--- a/src/utils/format.ts
+++ b/src/utils/format.ts
@@ -55,3 +55,26 @@ export function resolveFileUrl(url?: string): string {
export function resolveAvatarUrl(avatar?: string): string {
return resolveFileUrl(avatar)
}
+
+/**
+ * 商品规格展示:包规与单位直接拼接
+ * spec=20、unit=斤/箱 → 20斤/箱
+ */
+export function formatSpec(spec?: string | number | null, unit?: string | null): string {
+ const s = spec === null || spec === undefined ? '' : String(spec).trim()
+ const u = (unit ?? '').trim()
+ return `${s}${u}`
+}
+
+/**
+ * 零售价 = 售价 ÷ 包规(如 30¥/箱 ÷ 20斤/箱 = 2¥/斤)
+ * 保留两位小数并去掉尾零(2 → "2",2.50 → "2.5")
+ * 售价为空、包规非数字或 ≤0 时返回 null(不展示零售价)
+ */
+export function formatRetailPrice(price?: string | number | null, spec?: string | number | null): string | null {
+ if (price === null || price === undefined || price === '') return null
+ const p = Number(price)
+ const s = Number(spec)
+ if (!Number.isFinite(p) || !Number.isFinite(s) || s <= 0) return null
+ return String(Math.round((p / s) * 100) / 100)
+}